Тип возврата универсального объекта является результатом объединения методов - PullRequest
3 голосов
/ 08 марта 2019

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

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

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

1 Ответ

3 голосов
/ 08 марта 2019

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

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]

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


UPDATE

Вышеуказанное работает только с включенным --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.

Вам решать.

Удачи снова!

...