一、概念
在TypeScript中,我们可以使用接口来定义对象的类型。在面向对象的语言中,接口是一个很重要的概念,是对行为的一种抽象。但在TS中,接口是一个灵活的概念,除了可以表达对行为的抽象,也可以表示对象的形状(属性和方法)。
// 定义一个接口
interface Person {
name: string;
age: number;
}
// 定义对象的类型为上面的接口
let tom: Person = {
name: 'Tom',
age: 25
};
在上面的例子中,我们定义了一个 Person 接口,并设置其内部有两条属性,并定义属性的类型,此时该接口就表示某类对象的形状。然后定义了一个 tom对象,并设置类型为上面的接口 Person ,这样对象 tom 就受到了接口 Person 的约束,再给该对象赋值时,其内部组成必须与Person一致,多属性,或少属性都是不允许的,但顺序不同是允许的:
// 少属性
let tom: Person = {
name: 'Tom',
};
// 报错:TS2741: Property 'name' is missing in type '{ age: number; }' but required in type 'Person'.
// 多属性
let tom: Person = {
name: 'Tom',
age: 25,
sex: '男'
};
// 报错:TS2322: Type '{ name: string; age: number; sex: string; }' is not assignable to type 'Person'. Object literal may only specify known properties, and 'sex' does not exist in type 'Person'.
// 顺序不同 不会报错
let tom: Person = {
age: 25,
name: 'Tom',
};
二、接口属性
1、确定属性
就像上面的例子一样,接口声明的确定属性,在被设置为对象类型后,那么该对象赋值时,就必须存在接口声明的所有确定属性。
2、可选属性
如果我们想要使接口中的某个属性,在具体对象中可以存在,也可以不存在,那我们可以通过给属性添加 ?
标志符,来定义其为可选属性:
// 定义一个接口
interface Person {
name: string;
age?: number; // 通过 ? 设置为可选属性
}
// 存在可选属性
let tom: Person = {
name: 'Tom',
age: 25
};
// 不存在可选属性 不会报错
let tom: Person = {
name: 'Tom'
};
3、只读属性
如果我们想要使接口的属性,只有在具体对象创建时才能修改其值,再创建初始化之后,不能再做任何修改,那我们可以通过 readonly
或 ReadonlyArray<T>
来设置对应的属性为只读属性:
// 定义一个接口
interface Person {
readonly name: string; // 设置属性为只读属性
age: ReadonlyArray<number>; // 设置属性为只读数组属性
}
// 对象创建
let tom: Person = {
name: 'Tom', // 初始化赋值
age: [25,1,2] // 初始化赋值
};
// 修改只读属性
tom.name = 'jack'; // 报错
tom.age[0] = 0; // 报错
// 报错:TS2540: Cannot assign to 'name' because it is a read-only property.
4、任意属性
如果我们希望一个接口中,除了我们定义的属性之外,还允许有其他的任意属性,我们可以通过使用 索引签名 的形式来实现:
// 定义一个接口
interface Person {
name: string;
[propName: string]: any; // 设置索引签名
}
// 对象设置为接口类型
let tom: Person = {
name: 'Tom',
gender: 'male' // 添加的任意属性
};
但要注意的是,一个接口中,只能设置一个任意属性,而且接口所有的确定属性和可选属性的类型都必须是任意属性类型的子集,可以用 any
也可以用联合类型:
// 定义一个接口
interface Person {
name: string; // 确定属性
age?: number; // 可选属性
[propName: string]: string; // 任意属性
}
// 此时会报错:因为可选属性 age 是number属性,不属于任意属性类型 string 的子集
// 正确写法:
interface Person {
name: string; // 确定属性
age?: number; // 可选属性 真实的类型应该为:number | undefined
[propName: string]: string | number | undefined; // 任意属性
// 或者
// [propName: string]: any;
}
三、接口和类型别名的区别
1、概念
实际上在大多数情况下使用接口类型和类型别名的效果等价,只在某些特定的场景下有区别。TS的核心规则之一就是对值所具有的结构进行类型检查,接口就是专门用来为对象(包括函数)的类型进行命名并为其定义数据模型,相当于创建了一个新类型。而类型别名,仅仅是给一个类型起了一个新名字,不仅可以作用于对象,还可用于基本类型、联合类型、元组等任何类型。
2、对于数据类型的区别
对于对象和函数类型来说,接口和类型别名仅仅是语法不同,其余没有什么区别。
// 接口语法
// 对象类型
interface Point {
x: number;
y: number;
}
// 函数类型
interface SetPoint {
(x: number, y: number): void;
}
// 类型别名语法
// 对象类型
type Point = {
x: number;
y: number;
};
// 函数类型
type SetPoint = (x: number, y: number) => void;
对于联合类型、元组其他数据类型来说,接口无法定义,只能通过类型别名来进行定义:
// 基础类型
type Name = string;
// 联合类型
type PartialPoint = string | number;
// 元组
type Data = [number, string];
3、重复定义
对于接口来说,一个接口可以重复定义多次,如果有定义不同的属性,那么这些属性最后会合并到一个接口中,相当于第一次定义接口是创建,后续定义接口都相当于在创建好的接口上进行补充:
interface Point { x: number; }
interface Point { y: number; }
// 相当于
interface Point {
x: number;
y: number;
}
const point: Point = { x: 1, y: 2 }; //ok
而对于类型别名来说,不允许重复定义,如果重复定义,则会报错:
type Name = string;
type Name = number;
// 报错:TS2300: Duplicate identifier 'Name'.
4、扩展
两者的扩展方式不同,接口的扩展需要通过继承(extends)来实现,而类型别名的扩展就是通过交叉类型(&)来实现。而且两者扩展时,并不互斥,可以交叉进行扩展。
接口扩展:
interface PointX {
x: number
}
interface Point extends PointX {
y: number
}
// 结果相当于
interface Point {
x: number;
y: number;
}
类型别名扩展:
type PointX = {
x: number
}
type Point = PointX & {
y: number
}
// 结果相当于
type Point = {
x: number;
y: number;
}
两者交叉扩展:
// 类型别名
type PointX = {
x: number
}
// 接口
interface PointY {
x: number
}
// 交叉扩展
interface Point1 extends PointX {
y: number
}
type Point = PointY & {
y: number
}