Зачем оборачивать универсальную функцию и ожидать, что она вернет другой тип, а не возвращает значение напрямую?

#typescript #typescript-generics

Вопрос:

Проблема

Учитывая эти типы:

 type SelectAndInclude = {
  select: any;
  include: any;
};
type HasSelect = {
  select: any;
};
type HasInclude = {
  include: any;
};

type CheckSelect<T, S, U> = T extends SelectAndInclude
  ? "Please either choose `select` or `include`"
  : T extends HasSelect
  ? U
  : T extends HasInclude
  ? U
  : S;

declare function findMany<T extends {select?: string, include?: string}>(args: T): CheckSelect<T, Promise<1>, Promise<2>>;
 

Почему следующие функции имеют другой возвращаемый тип?

 function wrapperWorking<T extends {select?: string, include?: string}>(args: T){
  // correct return type inferred here
  return findMany(args); 
}

async function wrapperNotWorking<T extends {select?: string, include?: string}>(args: T){
  // result is always 1, which is wrong, since args is generic and not known here
  // result should probably have the type:
  // "Please either choose `select` or `include`" | 1 | 2
  const result = await findMany(args); 
  return resu<
}
 

Они по сути одинаковы, но их возвращаемый тип отличается.
Фактический и ожидаемый результат можно увидеть здесь:

 async function main() {
  const isErrorText = await wrapperWorking({select:"foo",include: "bar"});
  const is1 = await wrapperWorking({});
  const is2 = await wrapperWorking({select: "foo"});
  const is2Too = await wrapperWorking({include: "bar"});

  const shouldBeErrorTextButIs1 = await wrapperNotWorking({select:"foo",include: "bar"});
  const is1Too = await wrapperNotWorking({});
  const shouldBe2ButIs1 = await wrapperNotWorking({select: "foo"});
  const shouldBe2TButIs1Too = await wrapperNotWorking({include: "bar"});
}
 

Игровая площадка

Дополнительный контекст

Я хотел обернуть функцию libary для выполнения некоторых действий до и после ее вызова, например:

 async function wrapFindMany<T extends {select?: string, include?: string}>(args: T){
  await doSomethingBefore(args); // (e.g. async validation, sanitizing)
  const result = await findMany(args);
  await doSomethingAfterwards(result); // (e.g. writing to a DB)
  return resu<
}
 

Есть ли способ достичь этого без повторного объявления возвращаемого типа и выполнения хакерских any / unknown приведений?

Комментарии:

1. Не связано с вашим основным вопросом, но то, что вы хотите сделать, — это своего рода сквозная проблема. Взгляните на декораторы методов

2. Можете ли вы украсить существующие классы (импортированные из библиотеки) или даже объекты? Я также думаю, что выполнение чего-то вроде: @Before((args) => validateArgs(args)) @After((result) => save(result)) findMany.[...] , нарушает читаемость и не кажется естественным, особенно если это усложняется. Но я вижу ваши замечания, я думаю, это может иметь смысл в других сценариях.

3. Я не совсем уверен, но взгляните на фабрики декораторов . Может быть, вы можете обернуть свои сторонние библиотечные функции в свою собственную функцию-оболочку.

4. Соответствует ли этот подход вашим потребностям? Если это так, я с удовольствием напишу ответ; если нет, пожалуйста, уточните неудачный вариант использования. Обратите внимание, что «без повторного объявления возвращаемого типа и выполнения хакерских приведений», вероятно, невозможно… ну, вам не нужен any or unknown , но вам все равно нужны утверждения типа (кстати, термин «приведение» неоднозначен и его лучше избегать, поэтому я говорю «утверждение типа» здесь, поскольку это то, что есть у TS)

5. Кроме того, есть ли какая-то причина, по которой вы просто не делаете этого ? Объединение типов обещаний, по-видимому, сопряжено с опасностью, тогда как обещание типов объединения было бы просто отлично. Мне трудно представить, почему можно предпочесть первое последнему. Если вы согласны с переключением, то это было бы моим предложением, потому что оно ведет себя намного лучше. Дайте мне знать.