#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.62. Включите
--strict
ради себя (или, по крайней мере,--strictFunctionTypes
что, похоже, является причиной здесь), если сможете. Я уверен, что есть некоторые изменения, которые я мог бы внести, чтобыPush
работать без этого, но--strict
это настолько полезно, что я стараюсь тратить на это как можно меньше времени, насколько это возможно.3. обновлено версией
Push
, которая работает без--strict