什么是接口
在用 ts 开发项目时,使用最多的必然就是接口 interface
了。那什么是接口呢?
接口是用来描述一个具体的对象、函数或者类的类型,它的作用就是为这些类型命名,以及为你的代码提供代码定义契约。
它有时被称为"鸭子式辨型法",如果一个东西能长的像鸭子, 像鸭子一样游泳,一样叫,那么就可以认为这个东西就是一个鸭子。通过一个例子来说明:
function getStudentInfo(student: { age: number }) {
console.log(student.age);
}
const student = { age: 18, name: "张三" };
getStudentInfo(student); // 18
getStudentInfo
接收一个参数student
,它的类型是{ age: number }
,我们这里传入的数据多了个属性name
,但是 ts 进行类型检测的时候它并不会提示错误。
这是因为我们传入的对象当中包含age
属性,并且类型为number
,编译器只会检查那些必须的属性是否存在,并且其类型是否匹配。
这种检测方式称为鸭子辨型法。
上面代码中function getStudentInfo(student: { age: number })
,这样写很不优雅,我们用 interface
定义一个类型,如下:
interface Student {
age: number
}
function getStudentInfo(student: Student) {
console.log(student.age);
}
接口描述不同类型
1. 描述一个具体的对象
interface IOption {
label: string;
value: number;
}
const obj: IOption = {
label: 'key',
value: 1,
}
2. 描述函数类型
interface UserInfoFn {
(age: number, name: string) : string
}
const fn: UserInfoFn = function (age: number, name: string) {
return `${name}今年${age}岁了`
}
还可以描述函数的重载:
// 定义一个函数重载
interface UserInfoFn {
(age: number): string
(age: number, name: string): string
}
function getAdd(): UserInfoFn {
function add(age: number): string
function add(age: number, name: string): string
// 这里的name必须写成name?
function add(age: number, name?: string): string {
if (name) {
return name + age
} else {
return age + ''
}
}
return add
}
3. 描述类类型,使用 implements
实现接口
interface IAnimal {
name: string;
age: number;
}
class Panda implements IAnimal {
name = "mike";
age = 18;
}
4. 描述构造函数
interface People {
run: () => void;
}
interface PeopleConstructor {
new (name: string): People;
}
class Student implements People {
constructor(name: string) {}
run() {
console.log("run");
}
}
function createPeople(construction: PeopleConstructor, name: string): People {
return new construction(name);
}
const stu = createPeople(Student, "小明");
可索引的接口
当你不确定接口里面属性是什么时,可以使用可索引接口。它描述了对象索引的类型,还有相应的索引返回值类型。
interface StringArray {
[index: number]: string
}
let nameArray: StringArray = ['mike', 'jack'] // ok
let numArray: StringArray = [1, 2] // error
StringArray
定义了一个数字类索引,即 key
只能是数字,其实就是一个数组。
Typescript 支持 number
和 string
两种索引签名。下面是字符串类索引。
interface Obj1 {
[key: string]: string
}
let s1: Obj1 = {
name: 'xiaom',
female: 'nan'
}
两种索引可以同时使用,但是,由于使用 number 索引时,js 默认会将其转化为 string 再去索引对象,所以,数字索引的返回值必须是字符串索引返回值类型的子类型。
class Animal {
name: string = 'xiaom'
}
class Dog extends Animal {
breed: string = 'eat'
}
interface NotOkay {
[x: number]: Animal // 报错
[x: string]: Dog
}
interface Okay {
[x: number]: Dog // 正常
[x: string]: Animal
}
还需要注意一点,字符串的索引签名会让所有属性与其返回值类型相匹配。
interface St {
quantity: 20; // error
visible: true; // error
name: 'jack'; // ok
10: 0; // error
11: ''; // ok
[x: string]: string;
}
接口的属性
1. 可选属性
在可选属性名字定义的后面加一个?
符号,表示该属性"可有可无":
interface Persion {
name: string
age?: number
}
const p: Persion = {
name: 'xiao'
}
2. 只读属性
在只读属性名字定义的前面加上关键词 readonly
,表示该属性赋值后不可再改变:
interface IUser {
readonly name: string
readonly age: number
}
let myUser: IUser = {
name: 'mike',
age: 18
}
myUser.age = 20 // error
对象字面量的错误
有一种错误需要注意下,看如下的例子中的注释:
interface Persion {
name: string
age?: number
}
function getPersonName(person: Persion) {
return person.name
}
// person1里面多了一个sex属性,作为参数传递进去是不会报错的,只要参数里面有name这个属性即可
const person1 = {
name: 'jack',
sex: 'female'
}
getPersonName(person1)
// 如果这么写就会报错,这是因为ts会对字面量进行强校验,不只是要求有name属性,也不能有多余的属性
getPersonName({
name: 'jack',
sex: 'female'
})
接口继承
和类一样,接口也可以相互继承
interface People {
name: string;
}
interface Chi extends People {
language: string
}
interface Usa extends People {
skinColor: string
}
// 也可以继承多个
interface HalfBlood extends Chi, Usa{
weight: number
}
let Jessie:HalfBlood = {
name: 'Jessie',
language: 'Chinese',
skinColor: 'yellow',
weight: 50,
}
另外,需要注意一点:同名的接口会把各自的成员合并到一起。
interface people {
name: string
}
interface people {
age: number
}
const p1: people = {
name: 'mike',
age: 18
}
如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。
interface people {
height: string
}
interface people {
height: number // error
}
接口与type
、class
type
type
是 TypeScript 中用于定义类型别名、联合类型、交叉类型等复杂类型的声明方式。
interface
和 type
在编译后的 JavaScript 代码中被移除,因为它们仅在编译阶段用于类型检查。换句话说,type
不需要运行时信息。
相比之下,class
定义的类型信息会保留在编译后的代码中,因为它们包含实际的属性和方法实现,这些信息在运行时是必需的。
1. 类型别名(Type Aliases)
类型别名是给一个类型起一个新名字。例如:
type String1 = string;
2. 联合类型(Union Types)
联合类型表示一个值可以是多个类型中的一种。例如:
type StringOrNumber = string | number;
3. 交叉类型(Intersection Types)
交叉类型表示一个值必须满足多个类型的要求。例如:
type Name = { name: string };
type Age = { age: number };
type Person = Name & Age;
const t1: Person = {
name: 'xiaom',
age: 123
}
class
class
是一种定义类型和实现的方式。它既包含类型信息,也包含实际的属性和方法实现。接口只定义了类型,并不包括实现。
class
可以通过关键字 extends
实现类继承,还可以通过关键字 implements
实现接口实现。这使得 class
成为创建具有多层次结构和行为的对象的理想选择。
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
sayHello(): void {
console.log(`Hello, my name is ${this.name}`);
}
}
class Employee extends User {
role: string;
constructor(name: string, age: number, role: string) {
super(name, age);
this.role = role;
}
}
使用场景
-
type
适用于定义类型别名、联合类型、交叉类型等。 -
interface
主要用于定义对象的类型和形状,支持继承和实现。 -
class
既包含类型信息,也包含实际的属性和方法实现。
虽然 type
和 interface
在很多场景下可以互换使用,但它们在某些特定场景下有着各自的优势。type
更适用于组合不同类型,如联合类型、交叉类型等,而 interface
更适用于定义对象的形状,特别是在面向对象编程中。class
则提供了完整的类型定义和实现,可以在运行时进行实例化和操作。
在实践中,我们应该根据实际需求和场景选择合适的类型声明方式。例如,在定义一个复杂的对象类型时,可以使用 interface
;在组合不同类型时,可以使用 type
;在创建具有行为的对象时,可以使用 class
。