TypeScript 创建类型
前面的介绍中所使用的都是直接在变量、参数、函数后添加“字面量类型”,但实际的使用中为了更好的复用和代码的简洁会将类型提取出来单独使用。
TS 中提供了类型别名 type
和接口 interface
两种创建类型的方式,此外类 class
也可以作为一个单独的类型。
1. 类
TS 中对类(class
)提供了全面的支持,同时类也可以作为一个类型,但是区别是编译后会保留类代码,而其他方式创建的类型会被清除。
简介
类的属性可以在顶层声明,也可以在构造方法内部声明。对于顶层声明的属性,可以在声明时同时给出类型;如果不给出类型,TS 会认为都是 any;如果声明时有初始值,也可以不写类型,由 TS 推断:
class Point {
x; // 推断为 any
y = 1; // 推断为 number
z: boolean; // Property 'z' has no initializer and is not definitely assigned in the constructor.
}
2
3
4
5
TS 的编译选项 strictPropertyInitialization
开启时(默认开启)会检查属性是否由初始值,没有会报错。
属性前面加上 readonly
修饰符,表示该属性是只读的,实例对象不能修改这个属性。readonly 属性的初始值可以写在顶层属性,也可以写在构造方法中,构造方法中修改只读属性的初始值是允许的,因为初始值应以构造方法为准:
class A {
readonly id: string = "foo";
constructor() {
this.id = "bar"; // 正确
}
}
const a = new A();
a.id = "bar"; // 报错
2
3
4
5
6
7
8
9
10
类方法的类型声明与普通函数一致,可以使用参数默认值以及函数重载。另外构造方法不能声明返回值类型,因为它总是返回实例对象:
class Point {
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: number | string, y?: string) {
// ...
}
}
2
3
4
5
6
7
类中还有个特殊的方法叫做存取器(accessor),包括取值器(getter)和存值器(setter)两种方法。取值器用于读取某个属性,存值器用于写入某个属性:
class C {
_name = "";
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
}
2
3
4
5
6
7
8
9
TS 对存取器有以下规则:
- 如果某个属性只有 get 方法,没有 set 方法,那么该属性自动成为只读属性。
- TS 5.1 之前,set 方法的参数类型必须兼容 get 方法的返回值类型。之后的版本可以不兼容。
- get 方法和 set 方法的可访问性必须一致,都为公开方法或者私有方法。
类允许定义属性索引。
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
get(s: string) {
return this[s] as boolean;
}
}
2
3
4
5
6
7
上面示例中,[s:string]
表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数。
注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。
class MyClass {
[s: string]: boolean;
f() {
// 报错
return true;
}
}
2
3
4
5
6
7
上面示例中,属性索引的类型里面不包括方法,导致后面的方法 f()定义直接报错。
属性存取器视同属性,属性 inInstance 的读取器虽然是一个函数方法,但是视同属性,所以属性索引虽然没有涉及方法类型,但是不会报错。
class MyClass {
[s: string]: boolean;
get isInstance() {
return true;
}
}
2
3
4
5
6
7
类的 interface 接口
class 可以使用 implements
关键字指定一个 interface 或者 type 作为类型,但是正如 implements 的含义“实现”,interface 只是限制了类必须实现指定的类型,但是并不能代替类自身的类型声明。也就是说类的实际类型还是根据自身的实现而定,而不是 implements 的类型:
interface A {
x: number;
y?: number;
}
// 或 type A = { x: number; y?: number }
class B implements A {
x = 0;
}
const b = new B();
// b的类型不是A,仍热是根据类的定义确定的类型
b.y = 10; // 报错
2
3
4
5
6
7
8
9
10
11
12
13
注意
interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。
implements 后,还可以是另一个类,此时后面的类会被当作接口:
class Car {
id: number = 1;
move(): void {}
}
class MyCar implements Car {
id = 2; // 不可省略
move(): void {} // 不可省略
x = "a"; // 可以有更多属性
}
2
3
4
5
6
7
8
9
10
类可以实现多个接口(实质上就是接受多重限制),每个接口之间使用逗号分隔。但是同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代:
- 类的继承:继承一个类后就需要实现继承类的属性和方法,extends 可以和 implements 一起使用
class Car implements MotorVehicle {}
class SecretCar extends Car implements Flyable, Swimmable {}
2
3
- 接口的继承:将多个接口通过继承得到一个新的接口,前面的例子也可以如下实现
interface MotorVehicle {}
interface Flyable {}
interface Swimmable {}
interface SuperCar extends MotoVehicle, Flyable, Swimmable {}
class SecretCar implements SuperCar {}
2
3
4
5
6
7
与 interface 继承一样,当类需要实现的多个类型中有同名属性时,同名属性的类型不能冲突。
TS 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
class A {
x: number = 1;
}
interface A {
y: number;
}
let a = new A();
a.y = 10;
a.x; // 1
a.y; // 10
2
3
4
5
6
7
8
9
10
11
12
13
Class 类型
TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型:
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
// 错误
function createPoint(
PointClass: Point, // 可以用 typeof Point
x: number,
y: number
) {
return new PointClass(x, y); // PointClass为Point时返回值就是 Point
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JS 中,类只是构造函数的一种语法糖,本质上是构造函数的另一种写法。所以,类的自身类型可以写成构造函数的形式:
function createPoint(
PointClass: new (x: number, y: number) => Point, // 正确
// 还可以写成对象形式 PointClass: { new (x: number, y: number): Point}
x: number,
y: number
): Point {
return new PointClass(x, y);
}
2
3
4
5
6
7
8
Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。同样的如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合:
class Foo {
id!: number;
}
const bar = {
id: 10,
amount: 100,
};
function fn(arg: Foo) {}
fn(bar); // 正确
class Fxx {
id!: number;
amount!: number;
}
const f: Foo = new Fxx(); // 正确
const f2: Fxx = new Foo(); // 错误,Foo是Fxx的父类型,不能赋值给子类型
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用(相当于Object
或{}
类型)。
class Empty {}
function fn(x: Empty) {}
fn({});
fn(window);
fn(fn);
2
3
4
5
6
7
注意,确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TS 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。
class A {
private name = "a";
protected age = 18;
}
class B extends A {}
const a: A = new B(); // 继承的私有成员或保护成员能够兼容
class C {
private name = "c";
protected age = 18;
}
class D {
private name = "d";
protected age = 18;
}
const c: C = new D(); // 错误,非继承的私有成员或保护成员无法兼容
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
继承
类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。
class A {
a = 1;
greet() {
console.log("Hello, world!");
}
}
class B extends A {}
const b = new B();
b.greet(); // "Hello, world!"
console.log(b.a); // 1
2
3
4
5
6
7
8
9
10
11
12
子类可以覆盖基类的同名方法,还可以使用 super
关键字指代基类,但是子类的同名方法不能与基类的类型定义冲突:
class B extends A {
// string|undefined 兼容基类的类型 undefined
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name}`);
}
}
// string 不兼容基类类型 undefined,报错
// greet(name:string) {
// console.log(`Hello, ${name}`);
// }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
如果基类包括保护成员(protected 修饰符),子类可以将该成员的可访问性设置为公开(public 修饰符),也可以保持保护成员不变,但是不能改用私有成员(private 修饰符):
class A {
protected x: string = "";
protected y: string = "";
protected z: string = "";
}
class B extends A {
// 正确
public x: string = "";
// 正确
protected y: string = "";
// 报错
private z: string = "";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意,extends 关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数就可以了:
// 例一
// 这里的Array虽然传入了泛型,但是表示的还是数组的构造函数(不只表示类型)
class MyArray extends Array<number> {}
// 例二
class A {
greeting() {
return "Hello from A";
}
}
class B {
greeting() {
return "Hello from B";
}
}
interface Greeter {
greeting(): string;
}
interface GreeterConstructor {
new (): Greeter;
}
function getGreeterBase(): GreeterConstructor {
return Math.random() >= 0.5 ? A : B;
}
// getGreeterBase() 指定后返回 A 或者 B,也是一个构造函数
class Test extends getGreeterBase() {
sayHello() {
console.log(this.greeting());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意:
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
上面示例中,类 DogHouse 的顶层成员 resident 只设置了类型(Dog),没有设置初值。这段代码在不同的编译设置下,编译结果不一样。
如果编译设置的 target
设成大于等于 ES2022
,或者 useDefineForClassFields
设成 true
,那么下面代码的执行结果是不一样的。
const dog = {
animalStuff: "animal",
dogStuff: "dog",
};
const dogHouse = new DogHouse(dog);
console.log(dogHouse.resident); // undefined
2
3
4
5
6
7
8
上面示例中,DogHouse 实例的属性 resident 输出的是 undefined,而不是预料的 dog。原因在于 ES2022 标准的 Class Fields 部分,与早期的 TS 实现不一致,导致子类的那些只设置类型、没有设置初值的顶层成员在基类中被赋值后,会在子类被重置为 undefined,详细的解释参见官方 3.7 版本的发布说明。
解决方法就是使用 declare
命令,去声明顶层成员的类型,告诉 TS 这些成员的赋值由基类实现。
class DogHouse extends AnimalHouse {
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
2
3
4
5
6
7
上面示例中,resident 属性的类型声明前面用了 declare 命令,这样就能确保在编译目标大于等于 ES2022
时(或者打开 useDefineForClassFields
时),代码行为正确。
可访问性修饰符
类内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制,这三个修饰符的位置,都写在属性或方法的最前面:
- public:表示是公开成员,外部可以自由访问。public 是默认修饰符,可以省略不写。
- private:表示是私有成员,只能在当前类的内部使用,实例和子类中都不能使用该成员。
注意:子类不能定义父类私有成员的同名成员。
class A {
private x: number = 0;
}
const a = new A();
a.x; // 报错
class B extends A {
showX() {
console.log(this.x); // 报错
}
}
2
3
4
5
6
7
8
9
10
11
12
严格地说,private 定义的私有成员,并不是真正意义的私有成员。一方面,编译成 JavaScript 后,private 关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript 对于访问 private 成员没有严格禁止,使用方括号写法([])或者 in 运算符,实例对象就能访问该成员:
class A {
private x = 1;
}
const a = new A();
a["x"]; // 1
if ("x" in a) {
// 正确
}
2
3
4
5
6
7
8
9
10
由于 private 存在这些问题,加上它是 ES2022 标准发布前出台的,而 ES2022 引入了自己的私有成员写法#propName
。因此建议不使用 private,改用 ES2022 的写法,获得真正意义的私有成员:
class A {
#x = 1;
}
const a = new A();
a["x"]; // 报错
2
3
4
5
6
构造方法也可以是私有的,这就直接防止了使用 new 命令生成实例对象,只能在类的内部创建实例对象。这时一般会有一个静态方法,充当工厂函数,强制所有实例都通过该方法生成。
class Singleton {
private static instance?: Singleton;
private constructor() {}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
const s = Singleton.getInstance();
2
3
4
5
6
7
8
9
10
11
12
13
14
- protected:表示成员是保护成员,只能在类的内部使用,实例无法使用,但是子类内部可以使用。
class A {
protected x = 1;
}
class B extends A {
getX() {
return this.x; // 可以使用
}
}
const a = new A();
const b = new B();
a.x; // 报错
b.getX(); // 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
子类不仅可以拿到父类的保护成员,还可以定义同名成员:
class A {
protected x = 1;
}
class B extends A {
x = 2;
}
2
3
4
5
6
7
在 TS 中提供了一种实例属性初始化的简写形式(修饰符不能省略):
class A {
constructor(
public a: number,
protected b: number,
private c: number,
readonly d: number,
private readonly e: number // 修饰符可以与 readonly 组合使用
) {}
}
2
3
4
5
6
7
8
9
静态成员
类的内部可以使用 static
关键字,定义静态成员,静态成员是只能通过类本身使用的成员,不能通过实例对象使用。static 同样可以配合 public、private、protected 修饰符使用:
class MyClass {
public static x = 0;
public static printX() {
console.log(MyClass.x);
}
}
MyClass.x; // 0
MyClass.printX(); // 0
2
3
4
5
6
7
8
9
静态私有属性也可以用 ES6 语法的#前缀表示:
class MyClass {
static #x = 0;
}
2
3
public 和 protected 的静态成员可以被继承:
class A {
public static x = 1;
protected static y = 1;
}
class B extends A {
static getY() {
return B.y;
}
}
B.x; // 1
B.getY(); // 1
2
3
4
5
6
7
8
9
10
11
12
13
抽象类,抽象成员
TS 允许在类的定义前面,加上关键字 abstract,这种类叫做“抽象类”(abstract class)。抽象类只能当作基类使用,用来在它的基础上定义子类。:
abstract class A {
id = 1;
}
const a = new A(); // 报错
class B extends A {}
const b = new B(); // 正确
2
3
4
5
6
7
8
9
抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类:
abstract class A {
foo: number;
}
abstract class B extends A {
bar: string;
}
2
3
4
5
6
7
属性名与方法名有abstract
关键字时表示该方法需要子类实现,如果子类没有实现该成员就会报错:
abstract class A {
abstract foo: string;
}
// 报错:Non-abstract class 'B' does not implement all abstract members of 'A'
class B extends A {}
2
3
4
5
6
TIP
这里有几个注意点。
- 抽象成员只能存在于抽象类,不能存在于普通类。
- 抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加 abstract 关键字。
- 抽象成员前也不能有 private 修饰符,否则无法在子类中实现该成员。
- 一个子类最多只能继承一个抽象类。
this 问题
类的方法经常用到this
关键字,它表示该方法当前所在的对象。有些场合需要给出 this 类型,但是 JS 函数通常不带有 this 参数,这时 TS 允许函数增加一个名为this
的参数,放在参数列表的第一位,用来描述函数内部的 this 关键字的类型:
// 编译前
class A {
name = "A";
getName(this: A) {
return this.name;
}
}
const a = new A();
const b = a.getName;
b(); // 报错,此时的this是window(非严格模式下),与定义的 A 不一致
2
3
4
5
6
7
8
9
10
11
12
13
this 参数的类型可以声明为各种对象,TS 提供了一个 noImplicitThis 编译选项。如果打开了这个设置项,如果 this 的值推断为 any 类型,就会报错:
// noImplicitThis 打开
class Rectangle {
constructor(public width: number, public height: number) {}
getAreaFunction() {
return function () {
return this.width * this.height; // 报错,ts确定不了此处的this类型
};
}
}
2
3
4
5
6
7
8
9
10
11
在类的内部,this 本身也可以当作类型使用,表示当前类的实例对象,但是 this 类型不能应用于静态成员:
class Box {
contents: string = "";
set(value: string): this {
this.contents = value;
return this;
}
}
2
3
4
5
6
7
8
2. type
type
关键字用于创建一个类型别名,作用就是为任意类型取一个名称:
type A = string | number;
type B = "a" | "b";
type C = { x: number; y: number };
2
3
需要注意类型别名仅仅是为类型增加了一个名称,不代表相同类型的“不同版本”。类型是否兼容跟名称无关。
3. interface
基础
interface 是对象的模板,和前面提到的指定对象一样定义了一个对象的形状,TS 中类型名称通常使用大驼峰:
interface Person {
firstName: string;
lastName: string;
age: number;
}
const p: Person = {
firstName: "John",
lastName: "Smith",
age: 25,
};
2
3
4
5
6
7
8
9
10
11
上面的示例中 p
的类型就是接口 Person
,所以必须符合 Person 类型的结构。
interface 可以表示对象的各种语法,它的成员有 5 种形式:
- 对象属性
上面的示例中定义的都是对象属性,其他规则例如可选属性、只读属性在前文都已介绍,使用方式一致。
- 对象的属性索引
interface A {
[prop: string]: number;
}
2
3
- 对象方法
对象的方法共有三种写法:
// 写法一
interface A {
f(x: boolean): string;
}
// 写法二
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boolean): string };
}
2
3
4
5
6
7
8
9
10
11
12
13
14
属性名可以采用表达式,所以下面的写法也是可以的:
const f = "f";
interface A {
[f](x: boolean): string;
}
2
3
4
5
类型方法可以重载,interface 里面的函数重载,不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现:
interface A {
f(): number;
f(x: boolean): boolean;
f(x: string, y: string): string;
}
function MyFunc(): number;
function MyFunc(x: boolean): boolean;
function MyFunc(x: string, y: string): string;
function MyFunc(x?: boolean | string, y?: string): number | boolean | string {
if (x === undefined && y === undefined) return 1;
if (typeof x === "boolean" && y === undefined) return true;
if (typeof x === "string" && typeof y === "string") return "hello";
throw new Error("wrong parameters");
}
const a: A = {
f: MyFunc,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
4.函数
interface 也可以用来声明独立的函数(同样支持函数重载,不写方法名):
interface Add {
(x: number, y: number): number;
}
const myAdd: Add = (x, y) => x + y;
2
3
4
5
5.构造函数
interface 内部可以使用 new 关键字,表示构造函数:
interface ErrorConstructor {
new (message?: string): Error;
}
2
3
继承
interface 可以使用 extends
关键字继承其他 interface,继承后会将继承接口中的属性拷贝到当前接口中,这样就不必重复书写属性。interface 还允许多重继承:
interface Style {
color: string;
}
interface Shape {
name: string;
}
// 相当于:interface Circle { color:string; name:string; radius:number }
interface Circle extends Style, Shape {
radius: number;
}
2
3
4
5
6
7
8
9
10
11
12
上面示例中 Circle 是 Style 和 Shape 的子类型,当子接口于父接口存在同名属性时,子接口的属性会覆盖父接口的属性(但是子接口与父接口的同名属性必须是类型兼容的,否则会报错;同样的继承多个父接口且这些父接口存在同名属性时,也要求属性类型是兼容的,否则会报错。)。
extends 关键字也可以继承 type 定义的对象类型,如果 type 定义的不少对象,interface 就无法继承:
type Country = {
name: string;
capital: string;
};
interface CountryWithPop extends Country {
population: number;
}
2
3
4
5
6
7
8
interface 还可以继承 class,即继承该类的所有成员:
class A {
x: string = "";
y(): boolean {
return true;
}
}
// 继承了 x 属性和 y 方法
interface B extends A {
z: number;
}
2
3
4
5
6
7
8
9
10
11
12
如果某些类拥有私有成员和保护成员,interface 可以继承这样的类,但意义不大:
class A {
private x: string = "";
protected y: string = "";
}
interface B extends A {
z: number;
}
// 报错
const b: B = {};
// 报错
class C implements B {}
2
3
4
5
6
7
8
9
10
11
12
13
14
上面的示例中因为对象不能实现私有和保护成员,所以 B 不能应用到对象上,只能应用到其他类上。但此时 C 类却不是 A 类的子类,所以无法部署 A 类的 x 和 y 属性,导致报错。
接口合并
多个同名 interface 会合并成一个接口:
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
2
3
4
5
6
7
8
这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便。
举例来说,Web 网页开发经常会对 window 对象和 document 对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。
interface Document {
foo: string;
}
document.foo = "hello";
2
3
4
5
上面示例中,接口 Document 增加了一个自定义属性 foo,从而就可以在 document 对象上使用自定义属性。
同名接口合并时如果同一个属性被定义了多种不同的类型,则会报错;如果同一个方法名有不同的类型,则会发生函数重载,而且后面的定义比前面的定义具有更高的优先级:
interface Cloner {
a: number;
clone(animal: Animal): Animal;
}
interface Cloner {
a: string; // 报错
clone(animal: Sheep): Sheep;
}
// 合并后函数重载等同于
interface Cloner {
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个规则有一个例外是。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级:
interface A {
f(x: "foo"): boolean;
}
type X = "bar";
interface A {
f(x: X): boolean;
}
// 等同于
interface A {
f(x: "foo"): boolean;
f(x: X): boolean;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
上面示例中 foo
类型是通过字面量类型指定给参数的,所以合并后的优先级会更高。
如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型。
interface Circle {
area: bigint;
}
interface Rectangle {
area: number;
}
const s: Circle | Rectangle;
s.area; // bigint | number
2
3
4
5
6
7
8
9
10
11
type 与 interface 的异同
interface 与 type 的作用类型,都可以表示对象类型。几乎所有的 interface 类型都可以改写为 type 类型。
它们的相似之处:
- 都能为对象类型起名
区别有一下几点:
- type 能够表示非对象类型,而 interface 只能表示对象类型(包括数组、函数等)。
- interface 可以继承其他类型,type 不支持继承。
- 同名 interface 会自动合并,同名 type 则会报错。
- interface 不能包含属性映射,type 可以:
interface Point {
x: number;
y: number;
}
// 正确
type PointCopy1 = {
[Key in keyof Point]: Point[Key];
};
// 报错
interface PointCopy2 {
[Key in keyof Point]: Point[Key];
};
2
3
4
5
6
7
8
9
10
11
12
13
14
- this 关键字只能用于 interface:
// 正确
interface Foo {
add(num: number): this;
}
// 报错
type Foo = {
add(num: number): this;
};
2
3
4
5
6
7
8
9
- type 可以通过类型运算扩展原始数据类型,interface 继承不行:
// 正确
type MyStr = string & {
type: "new";
};
// 报错
interface MyStr extends string {
type: "new";
}
2
3
4
5
6
7
8
9
- interface 无法表达某些复杂类型(例如交叉类型和联合类型),type 可以:
综上所述,如果有复杂的类型运算,那么没有其他选择只能使用 type;一般情况下,interface 灵活性比较高,便于扩充类型或自动合并,建议优先使用。