Skip to content

类型兼容性 ​

TypeScript 中的类型兼容性基于结构子类型。结构类型是一种仅根据类型成员来关联类型的方法。这与名义打字形成对比。考虑以下代码:

ts
interface Pet {
  name: string;
}

class Dog {
  name: string;
}

let pet: Pet;
// OK, because of structural typing
pet = new Dog();

在 C# 或 Java 等名义类型语言中,等效代码将是一个错误,因为该类Dog没有显式地将自身描述为Pet接口的实现者。

TypeScript 的结构类型系统是根据 JavaScript 代码的典型编写方式设计的。由于 JavaScript 广泛使用函数表达式和对象字面量等匿名对象,因此用结构类型系统而不是名义类型系统来表示 JavaScript 库中的各种关系要自然得多。

关于稳健性的注释 ​

TypeScript 的类型系统允许某些在编译时无法得知的操作是安全的。当类型系统具有此属性时,我们就说它不是“健全的”。TypeScript 允许不合理行为的地方都经过仔细考虑,在本文档中,我们将解释这些情况发生的位置以及它们背后的动机场景。

开始 ​

TypeScript 结构类型系统的基本规则是与if至少具有相同的成员x兼容。例如,考虑以下代码,涉及一个名为Pet的接口,该接口具有一个属性:name

ts
interface Pet {
  name: string;
}

let pet: Pet;
// dog's inferred type is { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog;

为了检查是否dog可以分配给pet,编译器检查 的每个属性pet以在 中找到相应的兼容属性dog。在这种情况下,必须有一个名为name字符串的dog成员。确实如此,因此允许分配。

检查函数调用参数时使用相同的分配规则:

ts
interface Pet {
  name: string;
}

let dog = { name: "Lassie", owner: "Rudd Weatherwax" };

function greet(pet: Pet) {
  console.log("Hello, " + pet.name);
}
greet(dog); // OK

请注意,它dog有一个额外的owner属性,但这不会产生错误。Pet检查兼容性时仅考虑目标类型的成员(在本例中)。这个比较过程递归地进行,探索每个成员和子成员的类型。

但请注意,对象文字只能指定已知的属性。例如,因为我们明确指定了dog类型Pet,所以下面的代码是无效的:

ts
let dog: Pet = { name: "Lassie", owner: "Rudd Weatherwax" }; // Error

比较两个函数 ​

虽然比较基本类型和对象类型相对简单,但哪些类型的函数应被视为兼容的问题有点复杂。让我们从两个函数的基本示例开始,这两个函数仅在参数列表上有所不同:

ts
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

要检查 是否x可分配给y,我们首先查看参数列表。in 中的每个参数都x必须有一个y具有兼容类型的对应参数。请注意,不考虑参数的名称,仅考虑其类型。在这种情况下, 的每个参数x在 中都有对应的兼容参数y,因此允许赋值。

第二次赋值是错误的,因为y有一个必需的第二个参数x没有,所以不允许进行赋值。

您可能想知道为什么我们允许像示例中那样“丢弃”参数y = x。允许这种分配的原因是忽略额外的函数参数实际上在 JavaScript 中很常见。例如,Array#forEach向回调函数提供三个参数:数组元素、其索引以及包含的数组。尽管如此,提供仅使用第一个参数的回调非常有用:

ts
let items = [1, 2, 3];

// Don't force these extra parameters
items.forEach((item, index, array) => console.log(item));

// Should be OK!
items.forEach((item) => console.log(item));

现在让我们看看如何处理返回类型,使用两个仅返回类型不同的函数:

ts
let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });

x = y; // OK
y = x; // Error, because x() lacks a location property

类型系统强制源函数的返回类型是目标类型的返回类型的子类型。

函数参数双方差 ​

比较函数参数的类型时,如果源参数可分配给目标参数,则分配成功,反之亦然。这是不合理的,因为调用者最终可能会得到一个采用更专业化类型的函数,但会使用不太专业化的类型调用该函数。实际上,这种错误很少见,并且允许这种错误可以实现许多常见的 JavaScript 模式。一个简单的例子:

ts
enum EventType {
  Mouse,
  Keyboard,
}

interface Event {
  timestamp: number;
}
interface MyMouseEvent extends Event {
  x: number;
  y: number;
}
interface MyKeyEvent extends Event {
  keyCode: number;
}

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
  /* ... */
}

// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) =>
  console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)
);
listenEvent(EventType.Mouse, ((e: MyMouseEvent) =>
  console.log(e.x + "," + e.y)) as (e: Event) => void);

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

当这种情况发生时,您可以通过编译器标志让 TypeScript 引发错误strictFunctionTypes。

可选参数和其余参数 ​

当比较函数的兼容性时,可选参数和必需参数是可以互换的。源类型额外的可选参数不是错误,目标类型的可选参数在源类型中没有对应的参数也不是错误。

当函数具有剩余参数时,它被视为无限系列的可选参数。

从类型系统的角度来看,这是不合理的,但从运行时的角度来看,可选参数的想法通常没有得到很好的执行,因为传递undefined该位置对于大多数函数来说是等效的。

激励示例是函数的常见模式,该函数接受回调并使用一些可预测(对于程序员)但未知(对于类型系统)数量的参数来调用它:

ts
function invokeLater(args: any[], callback: (...args: any[]) => void) {
  /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));

// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));

具有重载的函数 ​

当函数具有重载时,目标类型中的每个重载都必须与源类型上的兼容签名相匹配。这确保了源函数可以在与目标函数相同的情况下被调用。

枚举 ​

枚举与数字兼容,数字也与枚举兼容。来自不同枚举类型的枚举值被认为是不兼容的。例如,

ts
enum Status {
  Ready,
  Waiting,
}
enum Color {
  Red,
  Blue,
  Green,
}

let status = Status.Ready;
status = Color.Green; // Error

课程 ​

类的工作方式与对象字面量类型和接口类似,但有一个例外:它们同时具有静态类型和实例类型。当比较类类型的两个对象时,仅比较实例的成员。静态成员和构造函数不影响兼容性。

ts
class Animal {
  feet: number;
  constructor(name: string, numFeet: number) {}
}

class Size {
  feet: number;
  constructor(numFeet: number) {}
}

let a: Animal;
let s: Size;

a = s; // OK
s = a; // OK

类中的私有成员和受保护成员 ​

类中的私有成员和受保护成员会影响它们的兼容性。检查类实例的兼容性时,如果目标类型包含私有成员,则源类型也必须包含源自同一类的私有成员。同样,这同样适用于具有受保护成员的实例。这允许类与其超类兼容,但不能与来自不同继承层次结构的类兼容,否则这些类具有相同的形状。

泛型 ​

由于 TypeScript 是一个结构类型系统,因此类型参数仅在作为成员类型的一部分使用时影响结果类型。例如,

ts
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;

x = y; // OK, because y matches structure of x

在上面,x和y是兼容的,因为它们的结构不以区分的方式使用类型参数。通过添加成员来更改此示例以Empty<T>显示其工作原理:

ts
interface NotEmpty<T> {
  data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y; // Error, because x and y are not compatible

通过这种方式,指定了类型参数的泛型类型的行为就像非泛型类型一样。

对于未指定类型参数的泛型类型,通过指定any代替所有未指定的类型参数来检查兼容性。然后检查生成的类型的兼容性,就像非泛型情况一样。

例如,

ts
let identity = function <T>(x: T): T {
  // ...
};

let reverse = function <U>(y: U): U {
  // ...
};

identity = reverse; // OK, because (x: any) => any matches (y: any) => any

高级主题 ​

子类型与分配 ​

到目前为止,我们使用了“兼容”,这不是语言规范中定义的术语。在 TypeScript 中,有两种兼容性:子类型和赋值。它们的不同之处仅在于,赋值扩展了子类型与规则的兼容性any,以允许enum与相应的数值进行赋值和赋值。

语言中的不同地方根据情况使用两种兼容机制之一。implements出于实际目的,类型兼容性由赋值兼容性决定,即使在and子句的情况下也是如此extends。

any、unknown、object、void、undefined、null和never可分配性 ​

下表总结了一些抽象类型之间的可分配性。行指示每个可分配的内容,列指示可分配给它们的内容。“ ✓strictNullChecks ”表示仅在关闭时兼容的组合。

任何未知目的空白不明确的无效的绝不
任意 →✓✓✓✓✓✕
未知 →✓✕✕✕✕✕
对象 →✓✓✕✕✕✕
无效→✓✓✕✕✕✕
未定义 →✓✓✓✓✓✕
空→✓✓✓✓✓✕
从来没有→✓✓✓✓✓✓

重申基础知识:

  • 一切都可以分配给它自己。
  • any和unknown就可分配给它们的内容而言是相同的,不同之处unknown在于不可分配给除 之外的任何内容any。
  • unknown和never是互为倒数的。一切都可以分配给unknown,never可以分配给一切。没有任何东西可以分配给never,unknown也不能分配给任何东西(除了any)。
  • void不可分配给任何内容或从任何内容分配,但以下情况除外:any、unknown、never、undefined和null(如果strictNullChecks关闭,请参阅表格了解详细信息)。
  • 当strictNullChecks关闭时,null与undefined类似never:可分配给大多数类型,大多数类型不可分配给它们。它们可以互相分配。
  • 当strictNullChecks打开时,null和 的undefined行为更像void: 不可分配给任何东西或从任何东西分配,除了any、unknown、never和void(undefined始终可分配给void)。