Тип возвращаемого универсального объекта является результатом цепочки методов

#typescript #typescript-generics

#typescript #typescript-generics

Вопрос:

Я хотел бы сделать следующее:

 var result = loader
    .add<number>(1)
    .add<string>("hello")
    .add<boolean>(true)
    .run();
  

Я хотел бы сконструировать этот теоретический loader объект таким образом, чтобы ТИП результата был [number, string, boolean] без необходимости вручную объявлять его как таковой. Есть ли способ сделать это в TypeScript?

Ответ №1:

ОБНОВЛЕНИЕ: в TypeScript 4.0 появятся переменные типы кортежей, что позволит более гибко управлять встроенными кортежами. Push<T, V> будет просто реализован как [...T, V] . Таким образом, вся реализация превращается в следующий относительно простой фрагмент кода:

 type Loader<T extends any[]> = {
    add<V>(x: V): Loader<[...T, V]>;
    run(): T
}
declare const loader: Loader<[]>;

var result = loader.add(1).add("hello").add(true).run(); //[number, string, boolean]
  

Ссылка на игровую площадку


ДЛЯ TS до версии 4.0:

К сожалению, в TypeScript нет поддерживаемого способа представления операции добавления типа в конец кортежа. Я вызову эту операцию, Push<T, V> где T — кортеж, а V — любой тип значения. Есть способ представить, добавляя значение к началу кортежа, который я вызову Cons<V, T> . Это потому, что в TypeScript 3.0 была введена функция для обработки кортежей как типов параметров функции. Мы также можем получить Tail<T> , который извлекает первый элемент (head) из кортежа и возвращает остальные:

 type Cons<H, T extends any[]> = 
  ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never;
type Tail<T extends any[]> = 
  ((...x: T) => void) extends ((h: infer A, ...t: infer R) => void) ? R : never;
  

Учитывая Cons и Tail , естественным представлением Push была бы эта рекурсивная вещь, которая не работает:

 type BadPush<T extends any[], V> = 
  T['length'] extends 0 ? [V] : Cons<T[0], BadPush<Tail<T>, V>>; // error, circular
  

Идея в том, что Push<[], V> должно просто быть [V] (добавление к пустому кортежу легко), и Push<[H, ...T], V> есть Cons<H, Push<T, V>> (вы держитесь за первый элемент H и просто нажимаете V на хвост T … затем добавьте H обратно к результату).

Хотя возможно обмануть компилятор, чтобы он разрешил такие рекурсивные типы, это не рекомендуется. Вместо этого я обычно выбираю некоторую максимально разумную длину кортежа, которую я хочу поддерживать для изменения (скажем, 9 или 10), а затем разворачиваю циклическое определение:

 type Push<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push1<Tail<T>, V>>
type Push1<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push2<Tail<T>, V>>
type Push2<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push3<Tail<T>, V>>
type Push3<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push4<Tail<T>, V>>
type Push4<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push5<Tail<T>, V>>
type Push5<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push6<Tail<T>, V>>
type Push6<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push7<Tail<T>, V>>
type Push7<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push8<Tail<T>, V>>
type Push8<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push9<Tail<T>, V>>
type Push9<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], PushX<Tail<T>, V>>
type PushX<T extends any[], V> = Array<T[number] | V>; // give up
  

Каждая строка, за исключением PushX , выглядит точно так же, как рекурсивное определение, и мы намеренно сокращаем это PushX , сдаваясь и просто забывая о порядке элементов ( PushX<[1,2,3],4> is Array<1 | 2 | 3 | 4> ).

Теперь мы можем сделать это:

 type Test = Push<[1, 2, 3, 4, 5, 6, 7, 8], 9> // [1, 2, 3, 4, 5, 6, 7, 8, 9]
  

Вооружившись Push , давайте зададим тип loader (оставляя реализацию на ваше усмотрение):

 type Loader<T extends any[]> = {
  add<V>(x: V): Loader<Push<T, V>>;
  run(): T
}
declare const loader: Loader<[]>;
  

И давайте попробуем это:

 var result = loader.add(1).add("hello").add(true).run(); //[number, string, boolean]
  

Выглядит неплохо. Надеюсь, это поможет; удачи!


Обновить

Вышеуказанное работает только с --strictFunctionTypes включенным. Если вы должны обойтись без этого флага компилятора, вы могли бы использовать следующее определение Push вместо:

 type PushTuple = [[0], [0, 0], [0, 0, 0],
    [0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
type Push<
    T extends any[],
    V,
    L = PushTuple[T['length']],
    P = { [K in keyof L]: K extends keyof T ? T[K] : V }
    > = P extends any[] ? P : never;
  

Он более лаконичен для небольших поддерживаемых размеров кортежей, что приятно, но повторение является квадратичным по количеству поддерживаемых кортежей (O (n, 2) роста) вместо линейного (O (n) роста), что менее приятно. В любом случае это работает с использованием сопоставленных кортежей, которые были введены в TS3.1.

Это зависит от вас.

Еще раз удачи!

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

1. Это потрясающее решение. Я полностью согласен с выбором разумного максимума. Тем не менее, я попытался использовать ваш пример точно так, как вы указали, и тип, выведенный из результата, является [number, ...undefined[]] . Возможно ли, что ваш пример не совсем корректен? Я использую typescript 3.1.6

2. Включите --strict ради себя (или, по крайней мере, --strictFunctionTypes что, похоже, является причиной здесь), если сможете. Я уверен, что есть некоторые изменения, которые я мог бы внести, чтобы Push работать без этого, но --strict это настолько полезно, что я стараюсь тратить на это как можно меньше времени, насколько это возможно.

3. обновлено версией Push , которая работает без --strict