Effective TypeScript
了解 type 和 interface 之间的区别
核心建议与设计选择
在 TypeScript 中,type(类型别名)和 interface(接口)都可以用来定义命名类型。多年来,两者之间的界限越来越模糊,但在大多数情况下,应该意识到它们剩余的区别并保持一致性。
- 一般规则: 在定义对象类型时,优先使用
interface。在必须使用或当type的语法更简洁时(例如联合类型和函数类型),才使用type。 - 命名规范: 不建议使用 'I' 或 'T' 作为类型名称的前缀(例如
IState或TState),这种习惯在现代 TypeScript 中被视为不良风格。
共同之处:何时两者可互换
对于简单的对象类型,interface 和 type 几乎没有区别,并且它们会受到相同的类型检查(例如严格属性检查)。
- 它们都可以定义相似的对象结构:
interface IState {
name: string;
capital: string;
}
type TState = {
name: string;
capital: string;
};- 它们都可以使用索引签名:
type TDict = { [key: string]: string };
interface IDict {
[key: string]: string;
}- 它们都可以定义函数类型,但
type的语法更简洁且更常用:
type TFn = (x: number) => string;
// 可选的 interface 形式,反映了函数是可调用对象的事实
interface IFn {
(x: number): string;
}-
它们都可以是泛型(Generic)的,例如
IBox<T>和TBox<T>。 -
它们可以互相扩展,尽管实现方式略有不同:
interface Person {
firstName: string;
lastName: string;
}
// 接口扩展类型
interface IStateWithBirthDate extends Person {
birth: Date;
}
// 类型别名使用交叉类型(&)扩展接口
type TStateWithBirthDate = Person & { birth: Date };- 类可以实现(
implements)接口或类型别名。
接口(interface)的独特优势
- 声明合并(Declaration Merging): 只有
interface支持声明合并(或称增强/扩展)。这意味着可以在同一个作用域内多次定义同名接口,TypeScript 会自动将它们合并为一个单一的定义。- 这种机制在类型声明文件(Declaration Files,Item 71)中尤为重要,常用于模型化不断演进的 API(例如在不同的 ES 版本中增强
Array接口)。
- 这种机制在类型声明文件(Declaration Files,Item 71)中尤为重要,常用于模型化不断演进的 API(例如在不同的 ES 版本中增强
interface IState {
name: string;
capital: string;
}
// 在其他地方补充定义
interface IState {
population: number; // 合并后 IState 包含所有三个属性
}
const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 578_000
}; // OK- 更严格的类型兼容性检查: 当一个接口尝试扩展另一个类型并修改其属性类型时,
interface extends提供了更严格的检查,能够捕获潜在的类型不兼容错误。而type &可能会生成一个结构上看似正确的交叉类型,但该类型在实际使用中却是不可用的。
interface Person {
name: string;
age: string;
}
// 接口扩展检查:会报错,因为 number 不能赋值给 string
interface IPerson extends Person {
// ~~~~~~~~~~~~~~ Interface 'IPerson' incorrectly extends interface 'Person'.
age: number;
}
// 类型交叉:不会立即报错,但生成了一个不可用的类型
type TPerson = Person & { age: number; }; // no error, unusable type- 类型显示的一致性: TypeScript 倾向于在错误消息和类型显示(Item 56)中始终使用接口的名称来指代它,而类型别名可能会被内联(inlining),即被其底层定义取代。
类型别名(type)的独特能力
type 别名在处理复杂类型结构和类型操作时,比 interface 更具表现力。
- 定义联合类型:
type是定义联合类型(Union Types)的唯一方式。
type AorB = 'a' | 'b';
type Result = number | string | Date;- 定义基本结构类型:
type是表达元组(Tuple)类型和数组类型的自然方式。
type Pair = [a: number, b: number];
type StringList = string[];- 复杂类型操作:
type必须用于结合了泛型(Generics)和类型操作(Type Operations)的复杂类型,例如映射类型(Mapped Types,Item 15)和条件类型(Conditional Types,Item 52)。
// 结合 keyof 和映射类型:
type PartialOptions<T> = { [K in keyof T]?: T[K] }; 类型别名内联行为的考量
类型别名的内联(inlining)是它们与接口最显著的运行时区别之一。
- 如果一个类型别名在一个导出函数中被使用,TypeScript 可能会在生成的
.d.ts文件中将其定义内联,而不是保留其命名。
// 原始 .ts 文件
export function getHummer() {
type Hummingbird = { name: string; weightGrams: number; };
const ruby: Hummingbird = { name: 'Ruby-throated', weightGrams: 3.4 };
return ruby;
};
// 生成的 .d.ts 文件中,类型被内联,名称 Hummingbird 消失
// get-hummer.d.ts
export declare function getHummer(): {
name: string;
weightGrams: number;
};-
相比之下,由于接口支持声明合并,如果它未被导出但在公共 API 中使用,TypeScript 会发出警告,因为它无法在类型声明文件中引用该名称。
-
这种内联行为(有时用于优化类型显示,Item 56)会影响最终的类型输出和错误信息的可读性,特别是在大型且复杂的类型中,应予以注意。
在 GitHub 上编辑
上次更新于