接口(Interface)
接口的定义
和java语言相同,TypeScript中定义接口也是使用 interface 关键字来定义:
interface IQuery {
page: number;
}
接口的好处
JavaScript中定义一个函数,用来获取一个用户的姓名和年龄的字符串:
const getUserInfo = function(user) {
return `name: ${user.name}, age: ${user.age}`
}
函数调用:
getUserInfo({name: "koala", age: 18})
这对于我们之前写JavaScript的时候,再正常不过了,但是如果这个getUserInfo在多人开发过程中, 如果它是个公共函数,多个开发者都会调用,如果不是每个人点进来看函数对应注释,可能会出现以下 问题:
// 错误的调用
getUserInfo() // Uncaught TypeError: Cannot read property 'name' of undefined
console.log(getUserInfo({name: "kaola"})) // name: kaola, age: undefined
getUserInfo({name: "kaola", height: 1.66}) // name: kaola, age: undefined
JS是弱类型的语言,所以并不会对我们传入的代码进行任何的检测。
TypeScript中的 interface 可以解决这个问题
const getUserInfo = (user: {name: string, age: number}): string => {
return `name ${user.name} age: ${user.age}`;
}
正确的调用时如下的方式:
getUserInfo({name: "kaola", age: 18});
如果调用者出现了错误的调用,那么TypeScript会直接给出错误的提示信息:
// 错误的调用
getUserInfo(); // 错误信息: An argument for 'user' was not provided.
getUserInfo({name: "coderwhy"}); // 错误信息: Property 'age' is missing in
type '{ name: string; }'
getUserInfo({name: "coderwhy", height: 1.88}); // 错误信息:类型不匹配
这时候你会发现这段代码还是有点长,代码不便阅读,这时候就体现了interface的必要性。
使用 interface 对 user 的类型进行重构
我们先定义一个IUser接口:
// 先定义一个接口
interface IUser {
name: string;
age: number;
}
接下来我们看一下函数如何来写:
const getUserInfo = (user: IUser): string => {
return `name: ${user.name}, age: ${user.age}`;
}
// 正确的调用
getUserInfo({name: "koala", age: 18})
// 错误的调用和之前一样,报错信息也相同不再说明。
接口中函数的定义再次改造
定义两个接口:
type IUserInfoFunc = (user: IUser) => string;
interface IUser {
name: string;
age: number;
}
接着二摸去定义函数和调用函数即可:
const getUserInfo: IUserInfoFunc = (user) => {
return `name: ${user.name}, age: ${user.age}`;
}
// 正确的调用
getUserInfo({name: "koala", age: 18})
// 错误的调用
getUserInfo();
接口中定义方法
定义接口时候不仅仅可以有属性,也可以有方法,如下:
interface IQuery {
page: number;
findOne(): void;
findAll(): void;
}
如果我们有一个对象是该接口类型,那么必须包含对应的属性和方法(无可选属性情况):
const q: IQuery = {
page: 1,
findOne() {
console.log("findOne");
},
findAll() {
console.log("findAll");
}
}
接口中定义属性
普通属性
上面的page就是普通属性,如果有一个对象是该接口类型,那么必须包含对应的普通属性。就不具体说了
可选属性
默认情况下一个变量(对象)是对应的接口类型,那么这个变量(对象)必须实现接口中所有的属性和方法。
但是,开发中为了让接口更加灵活,某些属性我们可能希望设计成可选的(想实现跨越实现,不想实现 也没有关系),这个时候就可以使用可选属性。
interface IQuery {
page: number;
findOne(): void;
findAll(): void;
isOneline?: string | number; // 是否出售中商品
delete?(): void
}
上面的代码中,增加了isOnline属性和delete方法,这两个都是可选的;
注意:可选属性如果没有赋值,那么获取到的值是 undefined ;对于可选方法,必须先进行判断,再调用,否则会报错;
const p: IQuery = {
page: 1,
findOne() {
console.log("findOne");
},
findAll() {
console.log("findAll");
}
}
console.log(p.isOneline); // undefined
p.delete(); // 不能调用可能是“未定义”的对象
正确的调用方式如下:
if(p.delete) {
p.delete();
}
大家可能会问既然是可选属性,可有可无的,那为什么还要定义呢?对比起完全不定义,定义可选的属性主要是:为了让接口更加的灵活,某些属性我们可能希望设计成可选,并且如果存在属性,能约束类型,而这也是十分关键的。
只读属性
默认情况下,接口中丁一一的属性可读可写:但是有一个关键字readonly,定义的属性值,不可以进行修改,强制修改后报错。
interface IQuery {
readonly page: number;
findOne(): void;
}
给page属性加了readonly关键字,再给它赋值会报错。
const q: IQuery = {
page: 1,
findOne() {
console.log("findOne");
}
};
q.page = 10; // Cannot assign to 'page' because it is a read-only property
任意属性
interface IQuery {
page: number;
delete?(): void;
[propName: string]: any;
}
const test: IQuery {
page: 10,
size: 20
}
propName可为任意值
函数类型接口
Interface还可以用来规范函数的形状。Interface里面需要列出参数列表返回值类型的函数定义。写法如下:
- 定义了一个函数接口
- 接口接收三个参数并且不返回任何值
- 使用函数表达式来定义这种形状的函数
interface Func {
// 定义与函数接收两个必选参数都是 number 类型,以及一个可选的字符串参数desc,这个函数不返回任何值
(x: number, y: number, desc?: string): void
}
const sum: Func = function(x, y, desc = '') {
// const sum: Func = function (x: number, y: number, desc: string): void
// ts 类型系统默认推论可以不必书写上述类型定义
console.log(desc, x + y)
}
sum(32, 22)
注意:不过上面的接口中只有一个函数,TypeScript会给我们一个建议,可以使用 type 来定义一个函数的类型:
type Func = (x: number, y: number, desc?: string) => void;
接口实现
接口除了定义某种类型规范,也可以和其它编程语言一样,让一个类去实现某个接口,那么这个类就必须明确去拥有这个接口中的属性和实现其方法:
下面的代码中会有关于修饰符的警告,暂时忽略,后面详细讲解 // 定义一个实体接口
interface Entity {
title: string;
log(): void
}
// 实现这样一个接口
class Post implements Entity {
title: string;
constructor(title: string) {
this.title = title;
}
log(): void {
console.log(this.title);
}
}
有些小伙伴的疑问?我定义了一个接口,但是我在继承这个接口的类中还要写接口的实现方法,那我不 如直接就在这个类中写实现方法岂不是更便捷,还省去了定义接口?
先记住两个字,规范
这个规可以达到你一看这个名字,就知道他是用来干什么的,并且可拓展,可维护。
- 在代码设计中,接口是一种规范;接口通常用于定义某种规范,类似于你必须遵守的协议,
- 站在程序角度上说接口只规定了类里必须提供的属性和方法,从而分离了规范和实现,增强了系统的可拓展性和可维护性;
接口的继承
和类一样,接口也能继承其他的接口。这相当于复制接口的所有成员。接口也是用关键字 extends 来继承。
interface Shape { // 定义接口Shape
color: string;
}
interface Square extends Shape { // 继承接口Shape
sideLength: number;
}
一个interface可以同时继承多个interface,实现多个接口成员的合并。用逗号隔开要继承的接口。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
需要注意的是,尽管支持继承多个接口,但是如果继承的接口中,定义的同名属性的类型不同的话是不能编译通过的。如下代码:
interface Shape {
color: string;
test: number;
}
interface PenStroke extends Shape{
penWidth: number;
test: string;
}
另外关于继承还有一点,如果现有一个类实现了Square接口,那么不仅仅需要实现Square的方法,也需要实现Square继承自接口中的方法,实现接口使用implements关键字
type和interface的区别
interface和type两个关键字因为其功能比较接近,常常引起疑问:应该再声明时候用 type,什么时候用 interface?
interface的特点如下:
- 同名interface自动聚合,也可以和已有的同名class聚合,适合做polyfill
- 自身只能表示object/class/function的类型
建议库的开发者所提供的公共 api 应该尽量用 interface/class,方便使用者自行扩展。
举例:
/**
* Cloud Studio使用的monaco版本较老0.14.3,和官方文档相比缺失部分功能
* 另外vscode有一些特有的功能,必须适配
* 故在这里手动实现作为补充
*/
declare module monaco {
interface Position {
delta(deltaLineNumber?: number, deltaColumn?: number): Position
}
}
// monaco 0.15.5
monaco.Position.prototype.delta = function(this: monaco.Position, deltaLineNumber = 0, deltaColumn = 0) {
return new monaco.Position(this.lineNumber + deltaLineNumber, this.column + deltaColumn);
}
与interface相比,type的特点如下:
- 表达功能更强大,不局限于 object/class/function
- 要扩展已有type需要创建新type,不可以重名
- 支持更复杂的类型操作
type Tuple = [number, string];
const a: Tuple = [2, 'sir'];
type Size = 'small' | 'default' | 'big' | number;
const b: Size = 24;
基本上所有用interface表达的类型都有其等价的type表达。但在实践的过程中,也发现了一种类型只能用interface表达,无法用type表达,那就是往函数上挂载属性。
interface FuncWithAttachment {
(param: string): boolean;
someProperty: number;
}
const testFunc: FuncWithAttachment = ...;
const result = testFunc('mike'); // 有类型提醒
testFunc.someProperty = 3; // 有类型提醒
type可以而interface不行
type可以声明基本类型别名,联合类型,元组等类型
// 基本类型别名
type Name = string
// 联合类型
interface Dog {
wong();
}
interface Cat {
miao();
}
type Pet = Dog | Cat
// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]
type语句中还可以使用typeof获取实例的类型进行赋值
// 当你想获取一个变量的类型时,使用typeof
let div = document.createElement('div');
type B = typeof div
type其他骚操作
type StringOrNumber = string | number;
type Text = string | { text: string };
type NameLookup = Dictionary<string, Person>;
type Callback<T> = (data: T) => void;
type Pair<T> = [T, T];
type Coordinates = Pair<number>;
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };
interface可以而type不行
interface能够声明合并
interface User {
name: string
age: number
}
interface User {
sex: string
}
/**
User 接口为 {
name: string
age: number
sex: string
}
*/
另外关于type的更多内容,可以查看文档:TypeScript官方文档
接口 vs 类型别名
类型别名看起来和接口非常类似,区别之处在于:
- 接口可以实现 extends 和 implements,类型别名不行。
- 类型别名并不会创建新类型,是对原有类型的引用,而接口会定义一个新类型。
- 接口只能用于定义对象类型,而类型别名的声明方式除了对象之外还可以定义交叉、联合、原始类型等。
类型别名是最初 TypeScript 做类型约束的主要形式,后来引入接口之后,TypeScript 推荐我们尽可能的使用接口来规范我们的代码。
类型别名在定义交叉类型、联合类型时特别好用,要注意类型别名与接口的区别。
接口的应用场景
在项目中究竟怎么用,开篇已经举了两个例子,在这里再简单写一点。在写查询参数检验的时候,或者 返回固定数据的时候,都会用到接口,看一段简单代码,已经看完了上面的文章,自己体会下吧。
import User from '../model/user';
import Good from '../model/good';
// 定义基本查询类型
// -- 查询列表时候使用的接口
interface Query {
page: number;
rows: number;
disabledPage?: boolean; // 是否禁用分页,true将会忽略`page`和`rows`参数
}
// 定义基本返回类型
type GoodResult<Entity> = {
list: Entity[];
total: number;
[propName: string]: any;
}
// 商品
export interface GoodsQuery extends Query {
isOnline?: string | number; // 是否出售中的商品
goodsNo?: string; // 商品编号
goodsName?: string; // 商品名称
}
export type GoodResult = QueryResult<Good>;