我想做以下事情:

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

我想以这样的方式构造这个理论上的 loader 对象,使结果的 TYPE 为 [number, string, boolean] 而无需手动声明它。有没有办法在 TypeScript 中做到这一点?

最佳答案

更新:TypeScript 4.0 将具有 variadic tuple types ,这将允许更灵活的内置元组操作。 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]
Playground link

对于 v4.0 之前的 TS:
不幸的是,TypeScript 中没有支持的方式来表示将类型附加到元组末尾的类型操作。我将此操作称为 Push<T, V>,其中 T 是一个元组,V 是任何值类型。有一种方法可以表示在元组开头添加一个值,我将其称为 Cons<V, T> 。这是因为在 TypeScript 3.0 中,向 treat tuples as the types of function parameters 引入了一项功能。我们还可以得到 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;
给定 ConsTailPush 的自然表示就是这个 recursive thing that doesn't work :
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 加到结果上)。
虽然可能诱使编译器允许此类递归类型,但 it is not recommended 。我通常做的是选择一些我想要支持修改的最大合理长度的元组(比如 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 之外的每一行看起来都像递归定义,我们通过放弃并忘记元素的顺序( PushXPushX<[1,2,3],4> )故意切断 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(n2) 增长)而不是线性(O(n) 增长),这不太好。无论如何,它通过使用在 TS3.1 中引入的 mapped tuples 来工作。
由你决定。
再次祝你好运!

关于typescript - 通用对象返回类型是方法链的结果,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/55066970/

10-16 17:49