从类型的默认参数推断 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
此处的必填字段较少,因此 Bar
是 Foo
的子类型,Bar
类型的任何元素都将被 getName
函数接受。也就是说,您可以在需要Foo
的任何地方安全地使用Bar
。 (正如@DDomen 在评论中所指出的,函数参数类型的行为不同)
让我解释一下协方差:
虽然您将类型 Foo
分配给 obj
,但它也适用于 getName(bar);
,因为 Bar extends Foo
(因此 Bar
是 Foo
的子类型)。
这是允许的,因为 Typescript 工程师设置类型系统的方式。他们决定应该允许对象的成员(即复杂类型,如形状和数组)接收其定义的类型以及该类型的任何子类型。
在类型理论中,这种行为可以描述为形状在其成员中协变。
其他类型系统不允许这种灵活性,即在这种情况下使用子类型。然后,此类类型系统将在形状的成员上调用 invariant,因为它们完全需要类型 Foo
,并且不允许 TypeScript 允许的这种灵活性。
还请注意,Bar
不必显式扩展 Foo
,因为 TypeScript 是结构化类型的。 TypeScript 比较两个对象的结构,看一个是另一个的子类型还是超类型。因此,除了interface Bar extends Foo
,您还可以将Bar
定义为:
interface Bar
name: string;
displayName: string;
并且getName
仍将正确键入,其参数obj
为Foo
。
何时使用泛型?
您不需要泛型,因为您不想将函数的不同部分相互关联。如果您想将输入参数与输出参数相关联,则可以有效地使用泛型。
查看您的代码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) => '1' | '2'
的函数分配给签名为 (obj: Bar) => string
的变量,这与逆变的定义相匹配:如果具有 @987654355,则类型 T
是 逆变 @,然后是T<P> <: T<S>
(Bar <: Foo
然后是Function<Foo> <: Function<Bar>
,<:
表示子类型)
@DDomen 谢谢,我改变了第一句话的表述。实际上,函数类型在 TS 中的参数类型是逆变的。但是,如果我有一个 getName
类型为 (obj: Foo = foo): string
的函数,如果 Bar <: Foo
(其中 <:
表示子类型),它将接受 Bar
类型的对象作为其参数。这不是协方差的定义(即,我可以提供Bar
类型的值,而它的超类型Foo
的值是预期的)协方差的定义还是我误导了?我最近才了解方差。非常感谢您的详尽评论!
@Andru 很抱歉最近很忙。你的更正是对的。您提到的是子类型化,您可以将Bar
类型的对象分配给Foo
类型的变量(给定Bar <: Foo
)。 Variance 描述了泛型类型的子类型,因此请回答“MyType<Foo>
和MyType<Bar>
之间的关系是什么?”这个问题。在 TS 中,所有用户定义的类型都是协变,所以MyType<Bar> <: MyType<Foo>
,只有函数是例外,它们是逆变 (Func<Foo> <: Func<Bar>
)。这意味着你可以做var f: (p: Bar) => ... = (p: Foo) => ...
但不能做var f: Foo = Bar
【参考方案2】:
问题是foo
不是T
类型,假设你想用接口Bar
实例化泛型:
const getName = (obj: Bar = foo): string =>
return obj.name;
;
您可以看到foo
不是Bar
(缺少displayName
属性)并且您不能将它分配给Bar
类型的变量。
您现在有三个选择:
强制将foo
用作通用T
:<T extends Foo>(obj: T = foo as T): string => ...
。但这是你能做的最糟糕的选择(可能导致意外的结果/错误)。
重载您的函数以匹配您的通用规范。如果您需要返回(并实际使用)您的通用实例,这很有用:
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 => ...
。这应该是你情况下最好的一个
【讨论】:
以上是关于从类型的默认参数推断 TypeScript 类型的主要内容,如果未能解决你的问题,请参考以下文章