Interface 接口
接口是TS中最重要的概念,TS用通过类型来对代码进行查验,而之前我们介绍的基础类型只是定义了简单的类型定义,而接口可以定义几乎任意结构(复杂结构)。
基本用法
// 示例 A
declare const myPoint: { x: number; y: number };
// 示例 B
interface Point {
x: number;
y: number;
}
declare const myPoint: Point;
typescript
示例 B 的好处在于,如果有人创建了一个基于 myPoint
的库来添加新成员, 那么他可以轻松将此成员添加到 myPoint
的现有声明中:
// Lib a.d.ts
interface Point {
x: number,
y: number
}
declare const myPoint: Point
// Lib b.d.ts
interface Point {
z: number
}
// Your code
myPoint.z // Allowed!
ts
TypeScript 接口是开放式的,这是 TypeScript 的一个重要原则,它允许你使用接口来模仿 JavaScript 的可扩展性。
由于TypeScript的声明合并策略,会将同名的一些可合并的声明进行合并,当同名的两个值或类型不能合并的时候,就会报错;或者可以合并的连个同名的值不符合要求,也会有问题。
注意:
- 不要把Interface理解为是在定义一个对象,而要理解为
{}
括号包裹的是一个代码块,里面是一条条声明语句,只不过声明的不是变量的值而是类型; - 声明也不用等号赋值,而是冒号指定类型;
- 每条声明之前用换行分隔即可,或者也可以使用分号或者逗号,都是可以的。
扩展用法
函数类型
接口可以描述普通对象,还可以描述函数类型,我们先看写法:
interface AddFunc {
// 调用签名: 由带有参数类型的参数列表和返回值类型组成
(num1: number, num2: number): number;
}
const add: AddFunc = (n1, n2) => n1 + n2;
const join: AddFunc = (n1, n2) => `${n1} ${n2}`; // 不能将类型'string'分配给类型'number'
add("a", 2); // 类型'string'的参数不能赋给类型'number'的参数
typescript
索引类型
可以使用接口描述索引的类型和通过索引得到的值的类型,比如一个数组['a', 'b']
,数字索引0
(数字类型)对应的通过索引得到的值为'a'
(字符)。我们可以同时给索引和值都设置类型,看下面的示例:
interface RoleDic {
[id: number]: string;
}
const role1: RoleDic = {
0: "super_admin",
1: "admin"
};
const role2: RoleDic = {
s: "super_admin", // error 不能将类型"{ s: string; a: string; }"分配给类型"RoleDic"。
a: "admin"
};
const role3: RoleDic = ["super_admin", "admin"];
typescript
也可以给索引设置readonly
,从而防止索引返回值被修改。
interface RoleDic {
readonly [id: number]: string;
}
const role: RoleDic = {
0: "super_admin"
};
role[0] = "admin"; // error 类型"RoleDic"中的索引签名仅允许读取
typescript
接口继承
接口继承相当于复制接口的所有成员(属性)。自 TypeScript 4.2 起,接口也可以继承由类型别名定义的对象类型,只要这些别名最终解析为对象形状。
用法:使用extends
关键词
示例场景:我们定义一个Vegetables
接口,它会对color
属性进行限制。再定义两个接口,一个为Tomato
,一个为Carrot
,这两个类都需要对color
进行限制,而各自又有各自独有的属性限制
interface Vegetables {
color: string;
}
interface Tomato {
color: string;
radius: number;
}
interface Carrot {
color: string;
length: number;
}
typescript
三个接口中都有对color
的定义,但是这样写很繁琐,所以我们可以用继承来改写:
interface Vegetables {
color: string;
}
interface Tomato extends Vegetables {
radius: number;
}
interface Carrot extends Vegetables {
length: number;
}
const tomato: Tomato = {
radius: 1.2 // error Property 'color' is missing in type '{ radius: number; }'
};
const carrot: Carrot = {
color: "orange",
length: 20
};
typescript
上面定义的 tomato
变量因为缺少了从Vegetables
接口继承来的 color
属性,从而报错。
一个接口可以被多个接口继承,同样,一个接口也可以继承多个接口,多个接口用逗号隔开。
interface Vegetables {
color: string;
}
interface Food {
type: string;
}
interface Tomato extends Food, Vegetables {
radius: number;
}
const tomato: Tomato = {
type: "vegetables",
color: "red",
radius: 1.2
}; // 在定义tomato变量时将继承过来的color和type属性同时声明
typescript
混合类型
JS的类型是灵活的,在JS中,函数是对象类型,对象可以有属性,所以有时对象即是一个函数,也包含一些属性。
包含函数与属性的类型应用场景,比如:
// javascript
let countUp = () => {
return ++countUp.count;
};
countUp.count = 0;
console.log(countUp()); // 1
console.log(countUp()); // 2
js
以使用混合类型接口来指定上面例子中 countUp
的类型:
interface Counter {
(): void; // 这里定义Counter这个结构必须包含一个函数,函数的要求是无参数,返回值为void,即无返回值
count: number; // 而且这个结构还必须包含一个名为count、值的类型为number类型的属性
}
// getCounter返回了一个Counter类型
const getCounter = (): Counter => { // 这里定义一个函数用来返回这个计数器类型
const c = () => { // 定义一个函数,逻辑和前面例子的一样
c.count++;
};
c.count = 0; // 再给这个函数添加一个count属性初始值为0
return c; // 最后返回这个函数对象
};
// 这里的counter是上面的Counter的一个具体实例
const counter: Counter = getCounter(); // 通过getCounter函数得到这个计数器
counter();
console.log(counter.count); // 1
counter();
console.log(counter.count); // 2
typescript
上面的例子中,getCounter
函数返回值类型为Counter
,它是一个函数,无返回值,即返回值类型为void
,它还包含一个属性count
,属性返回值类型为number
。
可选属性与只读属性
可选属性
有的时候,我们有些属性可能不存在也可能会有,这种情况,在定义Interface的时候,在属性名后面加个?
即可:
interface MyType {
color?: string; // 这里的color属性即是一个可有可无的属性
type: string;
}
const tmp: MyType = { type: 'string' } // 正确
typescript
只读属性
意义如其名,接口属性也可以设置只读属性,如下:
interface Role {
readonly 0: string;
readonly 1: string;
}
// 这里我们定义了一个角色字典,有 0 和 1 两种角色 id。下面我们定义一个实际的角色 数据,然后来试图修改一下它的值:
const role: Role = {
0: "super_admin",
1: "admin"
};
role[1] = "super_admin"; // Cannot assign to '0' because it is a read-only property
typescript
我们看到 TypeScript 告诉我们不能分配给索引0,因为它是只读属性。
readonly
与const
区别?
使用const
定义的常量定义之后不能再修改,这有点只读的意思,那它与readonly
的区别是什么?
用法解读:
如果是定义一个常量,那用const
,如果这个值是作为对象的属性,那请用readonly
。
我们来看下面的代码:
const NAME: string = "toimc";
NAME = "Haha"; // Uncaught TypeError: Assignment to constant variable
const obj = {
name: "toimc"
};
// 这里可以正常的修改,object是引用类型,里面的属性是可以修改的
obj.name = "Haha";
interface Info {
readonly name: string;
}
const info: Info = {
name: "toimc"
};
info["name"] = "Haha"; // Cannot assign to 'name' because it is a read-only property
typescript
我们可以看到上面使用const
定义的常量NAME
定义之后再修改会报错,但是如果使用const
定义一个对象,然后修改对象里属性的值是不会报错的。
如果我们要保证对象的属性值不可修改,需要使用readonly
。
绕开多余属性检查
接口的校验是严格的,在定义一个实现某个接口的值的时候,对于接口中没有定义的字段是不允许出现的,我们称这个为多余属性检查。
实际使用的时候,经常有的情况:并不希望 TypeScript 这么严格地对我们的数据进行检查。比如:
- 传递给接口多余的参数;
- 中间处理过程,多余的参数传递;
针对上述场景,那就需要绕开多余属性检查:
- 使用类型断言
- 添加索引签名
- 利用类型兼容性
类型断言名(推荐)
interface MyType {
color?: string;
type: string;
}
const getTypes = ({ color, type }: MyType) => {
return `A ${color ? color + " " : ""}${type}`;
};
getTypes({
type: "tomato",
size: 12,
price: 1.2
// 这里就是类型断言
} as MyTypes)
typescript
索引签名(推荐)
interface MyType {
color: string;
type: string;
[prop: string]: any;
}
const getTypes = ({ color, type }: MyType) => {
return `A ${color ? color + " " : ""}${type}`;
};
getTypes({
color: "red",
type: "tomato",
size: 12,
price: 1.2
});
typescript
利用类型兼容性(不推荐)
interface MyType {
type: string;
}
const getTypes = ({ type }: MyType) => {
return `A ${type}`;
};
const option = { type: "tomato", size: 12 };
getTypes(option);
typescript
我们将对象字面量赋给一个变量option
,然后getTypes
传入 option
,是因为直接将对象字面量传入函数,和先赋给变量再将变量传入函数,这两种检查机制是不一样的,后者是因为类型兼容性。
↑