类型兼容性
类型兼容性
TypeScript 使用结构化类型系统(Structural Type System),也称为”鸭子类型”(Duck Typing)。如果两个类型具有相同的结构,它们就是兼容的。
结构化类型系统
// TypeScript 关注的是"形状"而不是"名称"
interface Point {
x: number;
y: number;
}
interface NamedPoint {
x: number;
y: number;
name: string;
}
let point: Point = { x: 1, y: 2 };
let namedPoint: NamedPoint = { x: 1, y: 2, name: "origin" };
// Point 可以赋值给 NamedPoint(如果 NamedPoint 只要求 x 和 y)
// 但反过来不行,因为 NamedPoint 有额外的 name 属性
point = namedPoint; // 错误:缺少 name 属性
namedPoint = point; // 错误:缺少 name 属性
// 但如果 NamedPoint 的 name 是可选的,就可以赋值
interface OptionalNamedPoint {
x: number;
y: number;
name?: string;
}
let optionalNamed: OptionalNamedPoint = point; // ✅ 可以,因为 name 是可选的
基本兼容性规则
对象类型兼容性
// 目标类型必须包含源类型的所有必需属性
interface Source {
x: number;
}
interface Target {
x: number;
y: number;
}
let source: Source = { x: 1 };
let target: Target = { x: 1, y: 2 };
// target = source; // 错误:缺少 y 属性
source = target; // ✅ 可以:target 有 source 需要的所有属性
函数类型兼容性
函数类型的兼容性基于参数和返回类型:
// 参数兼容性:目标函数的参数类型必须是源函数参数类型的超类型
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // ✅ 可以:y 可以接受更多参数
// x = y; // 错误:x 不能接受第二个参数
// 返回类型兼容性:目标函数的返回类型必须是源函数返回类型的子类型
let x2 = () => ({ name: "Alice" });
let y2 = () => ({ name: "Alice", location: "Seattle" });
x2 = y2; // ✅ 可以:x2 的返回类型是 y2 返回类型的超类型
// y2 = x2; // 错误:缺少 location 属性
可选参数和剩余参数
// 可选参数和必需参数在兼容性上是可以互换的
function invokeLater(
args: any[],
callback: (...args: any[]) => any
) {
callback(...args);
}
invokeLater([1, 2], (x, y) => console.log(x + y)); // ✅
invokeLater([1, 2], (x?, y?) => console.log(x + y)); // ✅
枚举兼容性
enum Status {
Ready,
Waiting
}
enum Color {
Red,
Blue,
Green
}
let status = Status.Ready;
// status = Color.Red; // 错误:枚举类型不兼容
// 数字枚举与数字兼容
let num: number = Status.Ready; // ✅
类兼容性
class Animal {
feet: number;
constructor(name: string, numFeet: number) {
this.feet = numFeet;
}
}
class Size {
feet: number;
constructor(numFeet: number) {
this.feet = numFeet;
}
}
let a: Animal;
let s: Size;
a = s; // ✅ 可以:结构相同
s = a; // ✅ 可以:结构相同
// 但私有成员会影响兼容性
class Animal2 {
private feet: number;
constructor(name: string, numFeet: number) {
this.feet = numFeet;
}
}
class Size2 {
private feet: number;
constructor(numFeet: number) {
this.feet = numFeet;
}
}
let a2: Animal2;
let s2: Size2;
// a2 = s2; // 错误:私有成员来源不同,不兼容
// s2 = a2; // 错误:私有成员来源不同,不兼容
协变(Covariance)和逆变(Contravariance)
数组协变
// 数组是协变的:如果 A 是 B 的子类型,则 A[] 是 B[] 的子类型
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
let animals: Animal[] = [];
let dogs: Dog[] = [];
animals = dogs; // ✅ 协变:Dog[] 可以赋值给 Animal[]
// dogs = animals; // 错误:Animal[] 不能赋值给 Dog[]
// 但这是不安全的!
animals.push({ name: "Cat" }); // 现在 animals 包含了一个不是 Dog 的对象
// 但 dogs 引用的是同一个数组,所以 dogs 现在包含了 Cat
函数参数逆变
// 函数参数是逆变的:如果 A 是 B 的子类型,则 (x: B) => void 是 (x: A) => void 的子类型
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;
let animalHandler: AnimalHandler = (animal: Animal) => {
console.log(animal.name);
};
let dogHandler: DogHandler = (dog: Dog) => {
console.log(dog.name);
console.log(dog.breed);
};
// dogHandler = animalHandler; // ✅ 逆变:可以接受更通用的处理函数
// animalHandler = dogHandler; // 错误:不能接受更具体的处理函数
// 为什么?因为如果 animalHandler = dogHandler,然后调用:
// animalHandler({ name: "Cat" }); // dogHandler 期望 Dog,但收到了 Animal
函数返回类型协变
// 函数返回类型是协变的:如果 A 是 B 的子类型,则 () => A 是 () => B 的子类型
type AnimalFactory = () => Animal;
type DogFactory = () => Dog;
let animalFactory: AnimalFactory = () => ({ name: "Animal" });
let dogFactory: DogFactory = () => ({ name: "Dog", breed: "Labrador" });
animalFactory = dogFactory; // ✅ 协变:返回 Dog 的函数可以赋值给返回 Animal 的函数
// dogFactory = animalFactory; // 错误:返回 Animal 的函数不能赋值给返回 Dog 的函数
双变(Bivariance)
// 在 TypeScript 中,方法参数默认是双变的(为了兼容性)
interface Comparer<T> {
compare(a: T, b: T): number;
}
let animalComparer: Comparer<Animal> = {
compare(a: Animal, b: Animal) {
return a.name.localeCompare(b.name);
}
};
let dogComparer: Comparer<Dog> = {
compare(a: Dog, b: Dog) {
return a.breed.localeCompare(b.breed);
}
};
// 在 strictFunctionTypes 关闭时(默认),这是允许的
animalComparer = dogComparer; // 可能允许
dogComparer = animalComparer; // 可能允许
// 开启 strictFunctionTypes 后,方法参数变为逆变
类型兼容性检查
赋值兼容性
// TypeScript 检查赋值时的兼容性
let source: { x: number; y: number };
let target: { x: number };
target = source; // ✅ 可以:target 需要的属性 source 都有
// source = target; // 错误:source 需要 y,但 target 没有
函数调用兼容性
function processPoint(point: { x: number; y: number }) {
console.log(point.x, point.y);
}
let point = { x: 1, y: 2, z: 3 };
processPoint(point); // ✅ 可以:多余的属性 z 被忽略
// 但字面量对象会进行额外检查
processPoint({ x: 1, y: 2, z: 3 }); // 错误:多余的属性 z
泛型兼容性
// 泛型类型在没有指定类型参数时,兼容性检查会使用 any
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
x = y; // ✅ 可以:Empty 没有使用 T
// 但如果使用了类型参数,就不兼容了
interface NotEmpty<T> {
data: T;
}
let x2: NotEmpty<number>;
let y2: NotEmpty<string>;
// x2 = y2; // 错误:类型参数不同,不兼容
类型兼容性的实际应用
接口扩展
// 接口扩展体现了类型兼容性
interface Base {
id: number;
}
interface Derived extends Base {
name: string;
}
// Derived 兼容 Base
let base: Base = { id: 1 };
let derived: Derived = { id: 1, name: "test" };
base = derived; // ✅ 可以
类型收窄
// 类型兼容性使得类型收窄成为可能
function process(value: string | number) {
if (typeof value === "string") {
// TypeScript 知道这里 value 是 string
value.toUpperCase();
} else {
// TypeScript 知道这里 value 是 number
value.toFixed(2);
}
}
多态性
// 利用类型兼容性实现多态
class Animal {
makeSound() {
console.log("Some sound");
}
}
class Dog extends Animal {
makeSound() {
console.log("Woof!");
}
}
class Cat extends Animal {
makeSound() {
console.log("Meow!");
}
}
function makeAnimalsSound(animals: Animal[]) {
animals.forEach(animal => animal.makeSound());
}
makeAnimalsSound([new Dog(), new Cat()]); // ✅ 多态调用
最佳实践
- 理解结构化类型:TypeScript 关注结构而非名称
- 注意协变的不安全性:数组协变可能导致类型错误
- 使用 strictFunctionTypes:让函数参数类型检查更严格
- 理解逆变的意义:函数参数逆变是类型安全的保证
- 利用类型兼容性:编写更灵活的代码,同时保持类型安全