【问题标题】:TypeScript type inference from default parameter of type从类型的默认参数推断 TypeScript 类型
【发布时间】:2021-12-22 00:26:39
【问题描述】:

举个例子:

interface Foo {
  name: string;
}

interface Bar extends Foo {
  displayName: string;
}

const foo: Foo = { name: "foo" };
const bar: Bar = { name: "bar", displayName: "dn" };

const getName = <T extends Foo>(obj: T = foo): string => {
  return obj.name;
};

getName(foo); // Ok
getName(bar); // Ok

obj: T = foo 导致错误。即使T extends Foo,这是不被接受的。为什么会这样?有什么解决方法?

Example.

【问题讨论】:

    标签: typescript typescript-generics type-constraints


    【解决方案1】:

    您的getName 函数不需要Generic,可以为输入参数obj 使用类型注释Foo,即:

    const getName = (obj: Foo = foo): string => {
      return obj.name;
    };
    

    这是因为在 Typescript 中,每个复杂类型(例如对象和数组)在其成员中都是协变的

    换句话说:由于 TypeScript 知道 Foo 此处的必填字段较少,因此 BarFoo 的子类型,Bar 类型的任何元素都将被 getName 函数接受。也就是说,您可以在需要Foo 的任何地方安全地使用Bar。 (正如@DDomen 在评论中所指出的,函数参数类型的行为不同)

    让我解释一下协方差

    • 虽然您将类型 Foo 分配给 obj,但它也适用于 getName(bar);,因为 Bar extends Foo(因此 BarFoo 的子类型)。

    • 这是允许的,因为 Typescript 工程师设置类型系统的方式。他们决定应该允许对象的成员(即复杂类型,如形状和数组)接收其定义的类型以及该类型的任何子类型。

    • 在类型理论中,这种行为可以描述为形状在其成员中协变

    • 其他类型系统不允许这种灵活性,即在这种情况下使用子类型。然后,此类类型系统将在形状的成员上调用 invariant,因为它们完全需要类型 Foo,并且不允许 TypeScript 允许的这种灵活性。

    • 还请注意,Bar 不必显式扩展 Foo,因为 TypeScript 是结构化类型的。 TypeScript 比较两个对象的结构,看一个是另一个的子类型还是超类型。因此,除了interface Bar extends Foo,您还可以将Bar 定义为:

      interface Bar {
        name: string;
        displayName: string;
      }
      

      并且getName 仍将正确键入,其参数objFoo

    何时使用泛型

    您不需要泛型,因为您不想将函数的不同部分相互关联。如果您想将输入参数与输出参数相关联,则可以有效地使用泛型。

    查看您的代码TS Playground

    另请参阅 this related issue 关于您遇到的错误,即

    Type 'Foo' is not assignable to type 'T'.
      'Foo' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Foo'.(2322)
    

    【讨论】:

    • 链接指向一个不完整的文件
    • @Vivere 抱歉,已修复链接
    • 小心,typescript 本身不是 协变,但 typescript 中所有用户定义的类型都是(方差是类型的属性,而不是语言的属性)。同样在这里,您在打字稿中有少数 逆变 案例之一,因为getName 是一个函数。实际上,继续我们的示例,您可以将带有签名 (obj: Foo) =&gt; '1' | '2' 的函数分配给签名为 (obj: Bar) =&gt; string 的变量,这与逆变的定义相匹配:如果具有 @987654355,则类型 T逆变 @,然后是T&lt;P&gt; &lt;: T&lt;S&gt;Bar &lt;: Foo 然后是Function&lt;Foo&gt; &lt;: Function&lt;Bar&gt;&lt;: 表示子类型)
    • @DDomen 谢谢,我改变了第一句话的表述。实际上,函数类型在 TS 中的参数类型是逆变的。但是,如果我有一个 getName 类型为 (obj: Foo = foo): string 的函数,如果 Bar &lt;: Foo (其中 &lt;: 表示子类型),它将接受 Bar 类型的对象作为其参数。这不是协方差的定义(即,我可以提供Bar 类型的值,而它的超类型Foo 的值是预期的)协方差的定义还是我误导了?我最近才了解方差。非常感谢您的详尽评论!
    • @Andru 很抱歉最近很忙。你的更正是对的。您提到的是子类型化,您可以将Bar 类型的对象分配给Foo 类型的变量(给定Bar &lt;: Foo)。 Variance 描述了泛型类型的子类型,因此请回答“MyType&lt;Foo&gt;MyType&lt;Bar&gt; 之间的关系是什么?”这个问题。在 TS 中,所有用户定义的类型都是协变,所以MyType&lt;Bar&gt; &lt;: MyType&lt;Foo&gt;,只有函数是例外,它们是逆变 (Func&lt;Foo&gt; &lt;: Func&lt;Bar&gt;)。这意味着你可以做var f: (p: Bar) =&gt; ... = (p: Foo) =&gt; ... 但不能做var f: Foo = Bar
    【解决方案2】:

    问题是foo不是T类型,假设你想用接口Bar实例化泛型:

    const getName = (obj: Bar = foo): string => {
      return obj.name;
    };
    

    您可以看到foo 不是Bar(缺少displayName 属性)并且您不能将它分配给Bar 类型的变量。

    您现在有三个选择:

    1. 强制将foo 用作通用T&lt;T extends Foo&gt;(obj: T = foo as T): string =&gt; ...。但这是你能做的最糟糕的选择(可能导致意外的结果/错误)。

    2. 重载您的函数以匹配您的通用规范。如果您需要返回(并实际使用)您的通用实例,这很有用:

    const getName: {
      // when you specify a generic on the function
      // you will use this overload
      <T extends Foo>(obj: T): T;
    
      // when you omit the generic this will be the overload
      // chosen by the compiler (with an optional parameter)
      (obj?: Foo): Foo;
    }
    = (obj: Foo = foo): string => ...;
    
    1. 请注意,您的函数根本不使用泛型并将参数作为简单的Foo 传递:(obj: Foo = foo): string =&gt; ...。这应该是你情况下最好的一个

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-03-11
      • 2016-10-11
      • 2016-12-05
      • 2016-11-01
      • 2019-04-26
      • 2022-06-12
      • 2018-08-28
      • 2022-01-06
      相关资源
      最近更新 更多