从类型的默认参数推断 TypeScript 类型

Posted

技术标签:

【中文标题】从类型的默认参数推断 TypeScript 类型【英文标题】:TypeScript type inference from default parameter of type 【发布时间】: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.

【问题讨论】:

【参考方案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 类型的变量。

您现在有三个选择:

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

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

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 => ...;
    请注意,您的函数根本不使用泛型并将参数作为简单的Foo 传递:(obj: Foo = foo): string =&gt; ...。这应该是你情况下最好的一个

【讨论】:

以上是关于从类型的默认参数推断 TypeScript 类型的主要内容,如果未能解决你的问题,请参考以下文章

typescript 从子类调用的构造函数参数推断类型

如何使 TypeScript 从数组中推断参数的数量和类型

LayaBox---TypeScript---类型推论

TypeScript 类型推导及类型兼容性

来自接口实现的 Typescript 泛型推断

LayaBox---TypeScript---JavaScript文件类型检查