TypeScript 类型兼容性深度解析

TypeScript 类型兼容性深度解析

TypeScript-Handbook Deprecated, please use the TypeScript-Website repo instead TypeScript-Handbook 项目地址: https://gitcode.com/gh_mirrors/ty/TypeScript-Handbook

什么是类型兼容性

在 TypeScript 中,类型兼容性是基于结构子类型的。结构类型是一种仅基于成员来关联类型的方式,这与名义类型形成鲜明对比。理解这一核心概念对于掌握 TypeScript 的类型系统至关重要。

结构类型 vs 名义类型

结构类型系统关注的是类型的实际结构而非声明方式。让我们看一个简单示例:

interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
p = new Person();  // 有效,因为结构匹配

在 C# 或 Java 等名义类型语言中,这样的代码会报错,因为 Person 类没有显式声明实现了 Named 接口。TypeScript 采用结构类型系统是为了更好地适应 JavaScript 的编码模式,因为 JavaScript 中广泛使用匿名对象(如函数表达式和对象字面量)。

基本兼容规则

对象类型兼容性

TypeScript 结构类型系统的基本规则是:如果 y 至少具有与 x 相同的成员,则 x 与 y 兼容。例如:

interface Named {
    name: string;
}

let x: Named;
let y = { name: "Alice", location: "Seattle" };
x = y;  // 有效

编译器检查 y 是否能赋值给 x 时,会检查 x 的每个属性是否在 y 中有对应的兼容属性。这里 y 必须有名为 name 的字符串成员,它确实有,因此赋值被允许。

函数参数兼容性

同样的规则适用于函数调用参数检查:

function greet(n: Named) {
    console.log("Hello, " + n.name);
}
greet(y);  // 有效

注意 y 有一个额外的 location 属性,但这不会导致错误。在检查兼容性时,只考虑目标类型(这里是 Named)的成员。

函数类型兼容性

函数类型兼容性比原始类型和对象类型更为复杂,涉及参数列表和返回类型的比较。

参数数量差异

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

y = x;  // 有效
x = y;  // 错误

检查 x 是否可赋值给 y 时,我们首先查看参数列表。x 的每个参数必须在 y 中有对应的兼容类型参数(参数名不重要,只看类型)。这里 x 的每个参数在 y 中都有对应兼容参数,因此赋值允许。

第二个赋值错误是因为 y 需要一个 x 没有的第二个参数。

为什么允许"丢弃"参数?

这种设计源于 JavaScript 的常见模式。例如 Array#forEach 为回调函数提供三个参数,但通常我们只使用第一个:

let items = [1, 2, 3];
items.forEach(item => console.log(item));  // 常见用法

返回类型处理

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

x = y;  // 有效
y = x;  // 错误,因为 x() 缺少 location 属性

类型系统要求源函数的返回类型必须是目标函数返回类型的子类型。

高级兼容性话题

函数参数的双向协变

TypeScript 中函数参数比较是双向协变的,这在类型系统上是不健全的,但能支持许多常见 JavaScript 模式:

enum EventType { Mouse, Keyboard }

interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }

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

// 虽然不健全,但常见且有用
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + "," + e.y));

可以通过 strictFunctionTypes 编译器标志让 TypeScript 在这种情况下报错。

可选参数和剩余参数

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

剩余参数被视为无限系列的可选参数:

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

invokeLater([1, 2], (x, y) => console.log(x + ", " + y));

重载函数

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

枚举兼容性

枚举与数字兼容,数字也与枚举兼容。但不同枚举类型的值被视为不兼容:

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

let status = Status.Ready;
status = Color.Green;  // 错误

类兼容性

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

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;  // 有效,因为结构匹配

如果接口包含类型参数,则会影响兼容性:

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

x = y;  // 错误

对于未指定类型参数的泛型类型,通过用 any 替换所有未指定的类型参数来检查兼容性:

let identity = function<T>(x: T): T { /* ... */ }
let reverse = function<U>(y: U): U { /* ... */ }

identity = reverse;  // 有效

总结

TypeScript 的类型兼容性系统是其强大类型系统的核心部分。通过理解结构子类型、函数兼容性规则以及类、枚举和泛型的特殊处理方式,开发者可以更好地利用 TypeScript 的类型系统来构建健壮的应用程序,同时保持与 JavaScript 生态系统的良好互操作性。

记住,TypeScript 的设计始终以实用性和开发体验为核心,在某些情况下会为了支持常见 JavaScript 模式而放宽严格的类型安全要求。理解这些权衡有助于开发者做出更明智的类型设计决策。

TypeScript-Handbook Deprecated, please use the TypeScript-Website repo instead TypeScript-Handbook 项目地址: https://gitcode.com/gh_mirrors/ty/TypeScript-Handbook

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林菁琚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值