【问题标题】:How to prevent narrowing generic types while using literals with currying in typescript?在打字稿中使用带有currying的文字时如何防止缩小泛型类型?
【发布时间】:2019-09-09 06:41:31
【问题描述】:

我在为 Sanctuary(函数式编程的 js 库)设计类型时遇到问题。我想创建Ord 类型,代表任何具有自然顺序的值。 Ord 是:

  1. 一些内置的原始类型:numberstringDateboolean
  2. 实现所需接口的任何用户类型(compareTo 方法)
  3. Ords 的数组

为了表达它,我使用了(这对我来说很自然)联合类型(与使重复类型成为可能的轻微变通方法),例如:

type Ord = boolean | Date | number | string | Comparable<any> | OrderedArray

interface OrderedArray extends Array<Ord> {}

(我提到的任何代码都可以在 typescript playground 获得)。

然后,创建类型定义,例如compare 函数,我使用了带有类型约束的泛型类型,即类似(注意它是咖喱):

function compare<T extends Ord>(a: T): (b: T) => number

不幸的是,在使用带有文字参数的这种方法时,例如(compare(2)(3),这是完全有效的代码),打字稿错误,'3' is not assignable to '2'。似乎在提供第一个参数的情况下,typescript 将类型参数缩小到2(文字类型只包含2 值),而它应该只缩小到number。有什么办法可以阻止他这样做吗?或者任何其他工作方法?

【问题讨论】:

    标签: typescript type-inference


    【解决方案1】:

    如果编译器doesn't do it(例如这里T 的上下文类型包括stringnumberboolean),您也可以使用类型系统自己进行字面扩展:

    type Widen<T> = 
      T extends string ? string : 
      T extends number ? number : 
      T extends boolean ? boolean : 
      T;
    
    const compare = <T extends Ord>(x: T) => (y: Widen<T>) => 0;
    

    所以x 将保持狭窄,但y 将变宽。这将允许以下调用:

    compare(1)(3);  // <1>(x: 1) => (y: number) => number
    

    希望有所帮助;祝你好运!

    Link to code


    更新:如果您真的希望能够选择性地保持 T 狭窄,您可以做以下复杂的事情:

    type OptionallyWiden<X, T> = 
       [X] extends [never] ? 
         T extends string ? string : 
         T extends number ? number : 
         T extends boolean ? boolean : 
         T
      : T;
    
    const compare = <X extends Ord = never, T extends Ord = X>(x: T) => (
      y: OptionallyWiden<X, T>
    ) => 0;
    

    这会给你这种行为:

    compare(1)(3); // okay
    // const compare: <never, 1>(x: 1) => (y: number) => number
    
    compare<1 | 2>(1)(3); // error now
    //  ------------> ~
    // 3 is not assignable to 1 | 2
    // const compare: <1 | 2, 1 | 2>(x: 1 | 2) => (y: 1 | 2) => number
    

    它通过使用两个类型参数来工作...第一个,X,将默认为never,除非手动指定。如果Xnever,则T 的推断值将被扩大。如果X 不是never,则将使用指定的值而不是加宽。

    这里的灵活性值得怪异笨重的类型杂耍吗?我想这由你来决定,但如果我想要灵活地同时拥有窄和宽 T 值,我可能会保留原来的 &lt;T extends Ord&gt;(x: T) =&gt; (y: T) =&gt; 0,如果这是我想要的,只需手动加宽 T

    无论如何,希望这会再次有所帮助!

    Link to code

    【讨论】:

    • 嗯,随意扩大类型对我来说是不安全的。如果我想显式指定类型参数怎么办?号码联合类型?第二个参数会扩大到number,如果我提供无效值可能会失败?例如。与直觉相反,compare&lt;1 | 2 | 3&gt;(2)(42) 会通过类型检查。
    • 如果您想使用此方法获得更大的灵活性,我更新了此答案
    【解决方案2】:

    我最终想通了(至少以一种工作的方式,不一定是最好的)。事实证明,为所有具有文字表示的类型添加显式重载,因此可以缩小范围,修复它,例如:

    function compare(x: number): (y: number) => number;
    function compare(y: string): (y: string) => number;
    function compare(x: boolean): (y: boolean) => number;
    
    // and generic overload
    function compare<T extends Ord>(x: T): (y: T) => number;
    function compare<T extends Ord>(x: T): (y: T) => number {
      ...
    };
    

    我认为这些显式重载在签名解析中具有优先权,因此我的问题消失了,例如:

    compare(2)(3) // resolves to compare(x: number)(y: number)
    

    typescript playground 提供解决方案。

    【讨论】:

      【解决方案3】:

      如果您简化Ord 的定义,则所有测试都会通过。

      interface Comparable<T> {
        compareTo(other: T): number
      }
      
      type Ord<T> = T | Comparable<T> | Array<T>
      
      const compare = <T extends Ord<any>>(x: T) => (y: T) => 0;
      

      TypeScript Playground link.

      不过,我不确定这是否符合库的所有其他要求。

      【讨论】:

      • 不正确,它允许传递未实现Comparable 接口的用户类型的实例。实际上,我们抛弃了整个类型系统。
      • 你能在你的 Playground 例子中添加测试来断言这个规则吗?
      • @MichałKaczanowicz 根据您的定义,这也是有效的:compare([[1, 2]])([[3, 4], [4, 5, 6, 8]]) 方法调用?
      • 是的,我可以嵌套数组,比较的实现(为了简单起见,这里没有显示)在这种情况下递归地工作。
      • 好的。我认为缩小是因为在类型定义中 OrdOrderedArray 之间有循环引用。不知道如何避免这种情况。
      猜你喜欢
      • 2021-12-10
      • 1970-01-01
      • 2021-11-09
      • 1970-01-01
      • 2021-11-26
      • 2021-04-03
      • 2021-08-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多