类型兼容性

什么是类型兼容性

TypeScript的类型兼容性是指一个类型能否被赋值给另一个类型的规则系统。这是TypeScript类型系统中非常重要的概念,它决定了哪些操作是允许的,哪些会导致编译错误。

TypeScript的类型兼容性基于结构化类型(structural typing),这与名义类型(nominal typing)不同。在结构化类型系统中,类型的兼容性取决于类型的结构(即它包含哪些成员),而不是类型的名称。

基本类型兼容性

对于基本类型(number, string, boolean等),类型兼容性很简单:只有相同类型的值可以互相赋值。

typescript 复制代码
let num: number = 42;
let str: string = "hello";

num = str; // 错误:不能将类型"string"分配给类型"number"

对象类型兼容性

对象类型的兼容性更为复杂。TypeScript使用"鸭子类型"(duck typing)的方法来判断对象是否兼容:

typescript 复制代码
interface Named {
    name: string;
}

let x: Named;
let y = { name: "Alice", age: 30 };

x = y; // 正确,因为y有name属性

这里y能够赋值给x,因为y包含了x所需的所有属性(至少有一个name属性)。

函数类型兼容性

函数类型的兼容性考虑两个方面:参数类型和返回值类型。

参数类型

typescript 复制代码
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // 正确
x = y; // 错误

函数参数遵循"逆变"(contravariant)规则:如果要将函数y赋值给xx的每个参数必须能在y中找到对应类型的参数。

返回值类型

typescript 复制代码
let x = () => ({name: "Alice"});
let y = () => ({name: "Alice", age: 30});

x = y; // 正确
y = x; // 错误

返回值类型遵循"协变"(covariant)规则:源函数的返回值类型必须是目标函数返回值类型的子类型。

类类型兼容性

类的类型兼容性与对象字面量和接口类似,但有一点不同:类有静态部分和实例部分。比较两个类类型的对象时,只有实例成员会被比较,静态成员和构造函数不会影响兼容性。

typescript 复制代码
class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

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

let a: Animal;
let s: Size;

a = s;  // 正确
s = a;  // 正确

泛型类型兼容性

泛型的兼容性取决于类型参数如何被使用:

typescript 复制代码
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // 正确,因为Empty接口没有使用类型参数T

如果泛型类型使用了类型参数,则兼容性会受到影响:

typescript 复制代码
interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // 错误,因为data的类型不兼容

总结

TypeScript的类型兼容性规则是它的类型系统的核心部分。理解这些规则有助于:

  1. 编写更灵活且类型安全的代码
  2. 理解为什么某些赋值操作会被允许或拒绝
  3. 设计更好的接口和类型结构

记住TypeScript采用的是结构化类型系统,这与许多其他语言(如Java、C#)采用的基于名义的类型系统不同。这种设计使得TypeScript在保持类型安全的同时,提供了更大的灵活性。