首页 > 解决方案 > 类的泛型类型参数的推断失败取决于该类的属性

问题描述

我有一个相当简单的案例,Typescript 不推断类型。我想知道为什么 Typescript 会这样。

Typescript 类型推断失败的情况

如果您有以下类声明:

declare class Swine
{
    grunt(): void
}

declare abstract class Part<T> {}

declare class Hoof extends Part<Swine> {}

以及以下功能:

declare function getOwner<T>(part: Part<T>): T;

然后以下代码在 Typescript 编译器中失败:

getOwner(new Hoof())
  .grunt();

这是因为 Typescript 推断 getOwner 的返回类型是unknown,而不是Swine

奇怪但有效的解决方案

但是,如果我只是在animal: T的声明中添加一个属性Part则代码将开始工作

换句话说,如果声明Part变为:

declare abstract class Part<T>
{
  animal: T
}

Typescript 将能够推断getOwner(new Hoof()). 我已经验证了 Typescript 3.9.7 和 4.1.3 的这种行为。

问题

为什么 Typescript 会这样?它是否不应该保留泛型类型信息,即使T没有出现在任何类成员的签名中?

标签: typescript

解决方案


有关规范答案,请参阅关于未使用类型参数的 TypeScript 常见问题解答条目。

TypeScript 的类型系统主要是结构性的,而不是名义上的。这意味着如果您比较 typeAB,当且仅当它们具有相同的结构(例如,两者都是具有相同键和相同值类型的成员的对象类型)时,它们才被认为是相同的类型。什么类型AB命名(例如,我使用像“ A”和“ B”这样的名称是不相关的)或它们在哪里被声明(例如,有单独的声明,就像interface A { a: string }并且interface B { a: string }不会使它们成为单独的类型)都无关紧要.


特别是,鉴于声明

declare abstract class Part<T> { }

您可以看到,无论您为 指定什么类型T,生成的类型都是没有成员的空对象类型,因此与刚才的类型相同{}Part<swine>编译器真的看不出andPart<string>或其他任何东西之间有任何区别。

要看到这一点,请注意 TypeScript 不允许您使用var与原始声明不同类型的类型注释重新声明 a:

var bad: Swine;
var bad: string; // error!
//  ~~~
//  Subsequent variable declarations must have the same type.

但以下都不会产生任何编译器错误:

var okay: Part<Swine>;
var okay: {}; // no error
var okay: Part<string>; // no error
var okay: Part<unknown>; // no error

所以它们在 TypeScript 中确实是相同的类型。这意味着当你给编译器一个 type 的值时Part<Swine>,没有什么Swine像它一样的东西。namePart<Swine>只是一个name,与编译器如何处理类型无关。因此,试图T从类型的值推断Part<T>原则上是不可能的;推理失败,你得到unknown.

添加一个类型的属性TPart<T>改变一切。现在 and or or之间存在结构差异,编译器有一个句柄可以确定from 。Part<Swine>Part<string>Part<unknown>{}TPart<T>


如果我们想象通过查看将类型的字符串表示并将它们转换为类型的其他字符串表示的函数,将其从类型级别降低到值级别,那么您所做的模拟是这样的:

function part(T: string): string {
  return "{}";
}
const hoof = part("Swine");
function getOwnerType(partT: string): string {
  // how would you implement this ???
  return "unknown";
}
console.log(getOwnerType(hoof)); // unknown

的翻译declare abstract class Part<T> {}只是(T: string) => "{}")。由于part("Swine")产生与或其他任何东西相同的输出part("string"},即 string "{}",因此无法编写 reverse function getOwnerType()。另一方面,如果您的part()函数的输出实际上取决于其输入,则事情变得更加合理:

function part(T: string): string {
  return "{animal: " + T + "}";
}
const hoof = part("Swine");
function getOwnerType(partT: string): string {
  return (partT.match(/^{animal: (.*)}$/) ?? ["", "unknown"])[1];
}
console.log(getOwnerType(hoof)); // Swine

Playground 代码链接


推荐阅读