- TypeScript Tutorial 中文版 - 项目介绍
- TypeScript Tutorial 中文版 - Section 0. 前言
- TypeScript Tutorial 中文版 - Section 1. 入门
- TypeScript Tutorial 中文版 - Section 2. 基本类型
- TypeScript Tutorial 中文版 - Section 3. 控制流语句
- TypeScript Tutorial 中文版 - Section 4. 函数
- TypeScript Tutorial 中文版 - Section 5. 类
- TypeScript Tutorial 中文版 - Section 6. 接口
- TypeScript Tutorial 中文版 - Section 7. 高级类型
- TypeScript Tutorial 中文版 - Section 8. 泛型
- TypeScript Tutorial 中文版 - Section 9. 模块
- TypeScript Tutorial 中文版 - Section 10. Node.js
Section 6. 接口
接口
在本教程中,你将学习 TypeScript 中的接口,以及如何使用它们执行类型检查。
TypeScript 中的接口介绍
TypeScript 中的接口制定代码中的约束,也为类型检查提供显式的名称。让我们从一个简单的例子开始:
function getFullName(person: { firstName: string; lastName: string }) {
return `${person.firstName} ${person.lastName}`;
}
let person = {
firstName: 'John',
lastName: 'Doe',
};
console.log(getFullName(person));
输出:
John Doe
在这个例子中,TypeScript 编译器检查传递给 getFullName() 函数的参数,如果参数有 firstName 和 lastName 这两个字符串类型的属性,那么可以通过 TypeScript 的类型检查,否则会抛出错误提示。
通过代码可以清楚的发现,函数参数的 类型注释 让我们的代码变得难以阅读。为了解决这个问题,TypeScript 引入了接口的概念。
下面定义了一个 Person 接口,它有两个类型为字符串的属性:
interface Person {
firstName: string;
lastName: string;
}
按照惯例,接口名字都使用驼峰式,即使用大写字母分隔命名中的单词,比如 Person, UserProfile 和 FullName。
定义好 Person 接口之后你可以把它作为类型使用,也可以使用它为函数参数添加注释:
function getFullName(person: Person) {
return `${person.firstName} ${person.lastName}`;
}
let john = {
firstName: 'John',
lastName: 'Doe',
};
console.log(getFullName(john));
现在的代码比之前容易阅读很多。
getFullName() 函数接受任何具有 firstName 和 lastName 两个字符串类型的属性的对象作为参数,而它也不需要恰好只有这两个属性,如下所示,定义了一个具有四个属性的对象:
let jane = {
firstName: 'Jane',
middleName: 'K.'
lastName: 'Doe',
age: 22
};
因为 jane 对象具有 firstName 和 lastName 两个字符串类型的属性,你可以把它传入到 getFullName() 函数中,如下所示:
let fullName = getFullName(jane);
console.log(fullName); // Jane Doe
可选属性
接口可以拥有可选属性,要声明一个可选属性,你需要在属性名后添加 (?) 符号,如下所示:
interface Person {
firstName: string;
middleName?: string;
lastName: string;
}
在这个例子中,Person 接口有两个必选属性和一个可选属性。下面的例子演示了 Person 接口如何在 getFullName() 函数中使用:
function getFullName(person: Person) {
if (person.middleName) {
return `${person.firstName} ${person.middleName} ${person.lastName}`;
}
return `${person.firstName} ${person.lastName}`;
}
只读属性
如果属性只有在对象创建的时候可以被修改,可以在属性名前面加上 readonly 关键字:
interface Person {
readonly ssn: string;
firstName: string;
lastName: string;
}
let person: Person;
person = {
ssn: '171-28-0926',
firstName: 'John',
lastName: 'Doe',
};
在这个例子中,ssn 属性不能被修改:
person.ssn = '171-28-0000';
错误提示:
error TS2540: Cannot assign to 'ssn' because it is a read-only property.
函数类型
除了描述对象的属性外,接口也可以描述 函数类型。要描述函数类型的话,你需要将接口赋值成以下形式:
- 包含类型的参数列表
- 包含返回类型
如下所示:
interface StringFormat {
(str: string, isUpper: boolean): string;
}
现在,你可以使用这个函数类型接口了。下面演示如何声明具有函数类型的变量,并为其赋值:
let format: StringFormat;
format = function (str: string, isUpper: boolean) {
return isUpper ? str.toLocaleUpperCase() : str.toLocaleLowerCase();
};
console.log(format('hi', true));
输出:
HI
注意,参数名不需要匹配函数签名中的参数名字,下面的例子和上面的例子是等价的:
let format: StringFormat;
format = function (src: string, upper: boolean) {
return upper ? src.toLocaleUpperCase() : src.toLocaleLowerCase();
};
console.log(format('hi', true));
StringFormat 接口确保所有实现了它的函数调用方传入所需的参数:一个 字符串类型 的参数和一个 布尔值类型 的参数。
下面的代码也可以正常的工作,即使 lowerCase 函数没有第二个参数:
let lowerCase: StringFormat;
lowerCase = function (str: string) {
return str.toLowerCase();
};
console.log(lowerCase('Hi', false));
注意,第二个参数是在调用 lowerCase() 函数的时候传递的。
类类型
如果你使用过 Java 或者 C# 语言,你会发现接口的主要用途是定义不相关类之间的约定。例如下面的 Json 接口可以由任何不相关的类实现:
interface Json {
toJSON(): string;
}
下面声明了一个实现 Json 接口的类:
class Person implements Json {
constructor(private firstName: string, private lastName: string) {}
toJson(): string {
return JSON.stringify(this);
}
}
在 Person 类中我们实现了 Json 接口的 toJson() 方法。
下面的例子演示了如何使用 Person 类:
let person = new Person('John', 'Doe');
console.log(person.toJson());
输出:
{"firstName":"John", "lastName":"Doe"}
小结
- 接口制定代码中的约束,也为类型检查提供显式的名称;
- 接口可以有很多的可选属性和只读属性;
- 接口可以作为函数类型来使用;
- 接口经常被用作类类型来建立不相关类之间的约定。
扩展接口
在本教程中,你讲学习如何扩展接口,这样可以把一个接口的属性和方法复制到另外一个接口中。
扩展一个接口的接口
假设有一个名为 Mailable 的 接口,它包含 send() 和 queue() 两个方法:
interface Mailable {
send(email: string): boolean;
queue(email: string): boolean;
}
然后你有很多 类 已经实现了 Mailable 接口。现在,你想要在 Mailable 接口上添加一个新的方法, 表示它会延时发送邮件,如下所示:
later(email: string, after: number): void
给 Mailable 接口直接添加 later() 方法会破坏当前的代码,造成前后不兼容的问题。为了避免这个问题,你可以创建一个新的接口来扩展 Mailable 接口:
interface FutureMailable extends Mailable {
later(email: string, after: number): boolean;
}
使用 extends 关键字按照下面的语法来扩展一个接口:
interface A {
a(): void;
}
interface B extends A {
b(): void;
}
接口 B 扩展了接口 A,它有两个方法 a() 和 b()。与类相似,FutureMailable 接口从 Mailable 接口继承了 send() 和 queue() 方法。
下面的例子演示如何实现 FutureMailable 接口:
class Mail implements FutureMailable {
later(email: string, after: number): boolean {
console.log(`Send email to ${email} in ${after} ms.`);
return true;
}
send(email: string): boolean {
console.log(`Sent email to ${email} after ${after} ms. `);
return true;
}
queue(email: string): boolean {
console.log(`Queue an email to ${email}.`);
return true;
}
}
扩展多个接口的接口
一个接口可以扩展多个接口,创建所有接口的组合,如下所示:
interface C {
c(): void;
}
interface D extends B, C {
d(): void;
}
在这个例子中,接口 D 扩展了 B 和 C 接口,所以 D 接口有 B 和 C 接口的所有方法: a(), b() 和 c() 方法。
扩展类的接口
TypeScript 中允许接口扩展类,在这种情况下,接口会继承类的属性和方法,此外,接口可以继承类的私有成员和受保护成员,而不仅仅是公共成员。这意味着,当接口扩展具有私有成员和保护成员的类的时候,该接口只能有该接口所扩展的类或该类的子类中实现。
通过这种做法,可以把接口的使用范围限制为接口所继承的类或该类的子类,如果试图从一个不是接口继承的类或该类的子类来实现接口,则会抛出错误提示:
class Control {
private state: boolean;
}
interface StatefulControl extends Control {
enable(): void;
}
class Button extends Control implements StatefulControl {
enable() {}
}
class TextBox extends Control implements StatefulControl {
enable() {}
}
class Label extends Control {}
// Error: cannot implement
class Chart implements StatefulControl {
enable() {}
}
小结
- 接口可以扩展一个或多个现有的接口;
- 接口也可以扩展类,如果类包含私有成员或者受保护成员,则接口只能由该类或该类的子类实现。