TypeScript: есть ли какие-либо методы для прерывания рекурсивного вывода условного типа? - PullRequest
4 голосов
/ 14 марта 2019

Я пытаюсь ввести функцию, которая принимает аргумент, который сводит ряд элементов в одно накопленное значение.Вот упрощение того, что у меня есть:

// A stub type for the items
interface Item { t: 'item'; }

function paginate<A>(reduce: (acc: A, item: Item, index: number) => A): Promise<A> {
  // ... stub for actual implementation ...
  return true as unknown as Promise<A>;
}

Если я вызываю эту функцию, я не получаю желаемый тип логического вывода.Я надеюсь, что когда тип возвращаемого значения reduce известен, тогда первый аргумент (acc) также должен быть выведен как этот тип.

// expected: Promise<number>, inferred: Promise<{}>
const result = paginate((acc, item, index) => {
  acc; // expected: number, inferred: {}
  item; // expected: Item, inferred: Item
  index; // expected number, inferred: number

  return 5;
});

Я попытался использовать условные типы с логическим выводомчтобы решить эту проблему, но все варианты, которые я попробовал, либо потерпели неудачу, потому что они не ограничивают параметр универсального типа для acc чем-то конкретным, либо потерпели неудачу, потому что вывод рекурсивный.Я не совсем уверен.

type Reducer<A> = (acc: A, item: Item, index: number) => A;
type Accumulator<Reduce> = Reduce extends (acc: infer A, item: Item, index: number) => infer A ? A : never;

// Probably too loose (using an any)
function paginateA<R extends Reducer<any>>(reduce: R): Promise<Accumulator<R>> {
  // ...
  return true as unknown as Promise<Accumulator<R>>;
}

// expected: Promise<number>, inferred: Promise<number>
const resultA = paginateA((acc, item, index) => {
  acc; // expected: number, inferred: any
  item; // expected: Item, inferred: Item
  index; // expected number, inferred: number

  return 5;
});

// Probably too recursive (tried to circuit-break with only inferring the return type first)
function paginateB<R extends Reducer<ReturnType<R>>>(reduce: R): Promise<Accumulator<R>> {
  // ...
  return true as unknown as Promise<Accumulator<R>>;
}

// expected: Promise<number>, inferred: Promise<any>
const resultB = paginateB((acc, item, index) => {
  acc; // expected: number, inferred: any
  item; // expected: Item, inferred: Item
  index; // expected number, inferred: number

  return 5;
});

Существуют ли какие-либо методы для "прерывания цепи" рекурсивного вывода условного типа?Я вижу, Андерс упоминает, что некоторый класс рекурсивного вывода в порядке (хотя быстрая информация покажет тип как any), но я не могу понять условия, при которых это происходит.

Есть ли какая-то другая техника, которую я пропускаю?paginateA(), кажется, работает лучше всего, потому что, по крайней мере, он получил тип resultA правильно.Есть ли какая-то причина для этого?

Вот игровая площадка со всем приведенным выше кодом для манипуляции.

1 Ответ

2 голосов
/ 14 марта 2019

Как я уже сказал, я думаю, что конкретный вывод A из Reducer<A>, который вы пытаетесь достичь, может произойти автоматически в TypeScript 3.4 и выше, но это еще не было выпущено.Для TypeScript 3.3 и ниже:


Конкретная техника, которую я видел, использовалась в подобных случаях, когда вы хотите сказать

declare function f<T extends Something<T>>(x: T): void; // circularity error or other issue

, - оставить универсальный T неограниченными поместите ограничение на параметр функции через пересечение:

declare function f<T>(x: T & Something<T>): void; // works now

Я не могу найти каноническую документацию для этого, но я думаю, что это работает так, что когда вы вызываете f(x), компиляторпытается вывести T из x типа T & Something<T>.Поскольку x является T & Something<T>, это должно быть T, как работают пересечения.Таким образом, тип параметра x используется как T.Затем он проверит пересечение с Something<T>, и если это не сработает, вы получите ошибку компилятора.

Давайте попробуем это в вашем случае, но прежде чем мы это сделаем, большое предостережение: вы, вероятно, не можете заставить компилятор вывести параметр типа paginate() R из значения, которое вы передаетекак reduce, и выводят типы параметров значения, которое вы передаете как reduce из типа R, выведенного в вызове к paginate().То есть либо R будет выведено как Reducer<number>, но вам нужно будет аннотировать (acc:number, item:Item, index:number) ... или вам придется укажите R как Reducer<number>, и компилятор выведет типы acc, item, index.Вы хотите это обоими способами, но я не думаю, что компилятор достаточно умен.Или, по крайней мере, я не могу этого сделать.Итак, сейчас я предполагаю, что вы пытаетесь вывести R из полностью аннотированного обратного вызова reduce:

interface Item {
  t: "item";
}

type Reducer<A> = (acc: A, item: Item, index: number) => A;
type Accumulator<Reduce> = Reduce extends Reducer<infer A> ? A : never;

declare function paginateC<R extends Reducer<any>>(
  reduce: R & Reducer<ReturnType<R>> // intersection here
): Promise<Accumulator<R>>;

// Promise<number> as desired
const resultC = paginateC((acc: number, item: Item, index: number) => 5);

// error, string is not number:
paginateC((x: number, y: Item, z: number)=>"string"); 

// okay, since string is a subtype of unknown
paginateC((x: unknown, y: Item, z: number)=>"string")

// also works with any
paginateC((x: any, y: Item, z: number)=>"string")

Так что все в порядке.


Вернемся к проблеме с выводом acc, item, index ... Вы всегда можете указать R следующим образом:

// acc, item, index inferred from specified R
paginateC<Reducer<number>>((acc, item, index) => 5);

, но вы не хотите этого делать,

По правде говоря, я не ожидал бы, что компилятор сузит параметр acc до желаемого значения A, поскольку параметры функции всегда можно безопасно расширять ( контрастность параметров функции ),Компилятор, вероятно, оставит acc как что-то широкое, например {}, unknown или any, если вы не аннотируете его или не задаете R вручную, как указано выше.

I am немного удивлен, что он не сужает item и index до Item и number соответственно вместо того, чтобы оставить их как any.Но, вероятно, сделать не так уж и много, так как это известное ограничение вывода типа, что оно не происходит за несколько проходов.Ну хорошо.


Хорошо, надеюсь, это поможет.Удачи!

...