【问题标题】:Generic object return type is result of method chaining通用对象返回类型是方法链接的结果
【发布时间】:2023-03-07 23:05:01
【问题描述】:

我想做以下事情:

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

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

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案1】:

    更新:TypeScript 4.0 将具有 variadic tuple types,这将允许更灵活的内置元组操作。 Push&lt;T, V&gt; 将简单地实现为[...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&lt;T, V&gt;,其中T 是一个元组,V 是任何值类型。 有一种方法可以在元组的开始上表示预先一个值,我称之为Cons&lt;V, T&gt;。这是因为在 TypeScript 3.0 中,treat tuples as the types of function parameters 引入了一项功能。我们还可以得到Tail&lt;T&gt;,它将第一个元素(头部)从元组中拉出并返回其余部分:

    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 的自然表示将是这个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&lt;[], V&gt; 应该只是 [V] (附加到一个空元组很容易),而 Push&lt;[H, ...T], V&gt; 是 Cons&lt;H, Push&lt;T, V&gt;&gt; (你抓住第一个元素 H 并按下 @987654345 @ 加到尾部 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 之外的每一行看起来都像递归定义,我们通过放弃并忘记元素的顺序(PushX&lt;[1,2,3],4&gt; 是Array&lt;1 | 2 | 3 | 4&gt;)故意在PushX 处截断。

    现在我们可以这样做了:

    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 来工作。

    这取决于你。

    再次祝你好运!

    【讨论】:

    • 这是一个很棒的解决方案。我完全可以选择一个合理的最大值。但是,我尝试完全按照您提供的示例使用您的示例,从结果中推断出的类型是[number, ...undefined[]]。您的示例是否可能不太正确?我正在使用打字稿 3.1.6
    • 如果可以的话,为了你自己的缘故打开--strict(或者至少--strictFunctionTypes这似乎是这里的原因)。我确信在没有它的情况下我可以做出一些改变让Push 工作,但--strict 非常有用,我会尽量少用时间来关闭它。
    • 更新了 Push 的版本,可以在没有 --strict 的情况下使用
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-10
    • 2012-05-04
    • 1970-01-01
    • 2018-02-23
    • 1970-01-01
    相关资源
    最近更新 更多