说说你对 TypeScript 中接口的理解?应用场景?
问题解析
接口是 TypeScript 的核心特性之一,考察候选人对接口的定义、使用方式、高级特性以及实际应用场景的理解。需要能够清晰区分接口与类型别名的差异。
核心概念
- 接口(Interface):描述对象的结构契约
- 可选属性:使用
?标记 - 只读属性:使用
readonly标记 - 索引签名:处理动态属性
- 接口继承:使用
extends扩展接口 - ** duck typing**:鸭子类型,结构类型系统
详细解答
1. 是什么
接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。
简单来讲,一个接口所描述的是一个对象相关的属性和方法,但并不提供具体创建此对象实例的方法。
TypeScript 的核心功能之一就是对类型做检测,虽然这种检测方式是"鸭式辨型法",而接口的作用就是为这些类型命名和为你的代码或第三方代码定义一个约定。
2. 使用方式
2.1 基础接口定义
interface User {
name: string;
age: number;
}
const getUserName = (user: User) => user.name;
// 正确的使用
getUserName({ name: "Tom", age: 25 }); // ok
// 错误的使用
// getUserName({ name: "Tom" }); // Error: 缺少属性 "age"
2.2 可选属性
使用 ? 标记可选属性:
interface User {
name: string;
age?: number; // 可选属性
}
const tom: User = { name: "Tom" }; // ok
const jerry: User = { name: "Jerry", age: 25 }; // ok
这时候 age 属性则可以是 number 类型或者 undefined 类型。
2.3 只读属性
使用 readonly 声明只读属性:
interface User {
name: string;
age?: number;
readonly isMale: boolean; // 只读属性
}
const tom: User = {
name: "Tom",
isMale: true
};
// tom.isMale = false; // Error: 无法分配到 "isMale" ,因为它是只读属性
2.4 函数类型
接口可以描述函数类型:
interface User {
name: string;
age?: number;
readonly isMale: boolean;
say: (words: string) => string; // 函数类型
}
const tom: User = {
name: "Tom",
isMale: true,
say: (words: string) => `Tom says: ${words}`
};
2.5 额外属性检查
如果传递的对象包含接口未定义的属性,TypeScript 会报错:
interface User {
name: string;
age: number;
}
// 解决方法一:类型断言
getUserName({ color: 'yellow', name: "Tom", age: 25 } as User);
// 解决方法二:添加字符串索引签名
interface User {
name: string;
age: number;
[propName: string]: any; // 允许任意额外属性
}
2.6 接口继承
接口可以继承其他接口,使用 extends 关键字:
interface Father {
color: string;
}
interface Mother {
height: number;
}
// 单继承
interface Son extends Father {
name: string;
age: number;
}
// 多继承
interface Son extends Father, Mother {
name: string;
age: number;
}
const son: Son = {
color: "yellow",
height: 180,
name: "Tom",
age: 10
};
3. 应用场景
3.1 函数参数类型约束
// 多人开发时,使用接口定义参数变量,避免运行时错误
interface IUser {
name: string;
age: number;
}
const getUserInfo = (user: IUser): string => {
return `name: ${user.name}, age: ${user.age}`;
};
// 正确的调用
getUserInfo({ name: "koala", age: 18 });
// 错误的调用会在编译时报错
// getUserInfo({ name: "koala" }); // Error: 缺少属性 "age"
3.2 类实现接口
interface Animal {
name: string;
makeSound(): void;
}
class Dog implements Animal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound() {
console.log("Woof!");
}
}
3.3 定义对象字面量类型
interface Point {
x: number;
y: number;
}
function printPoint(p: Point) {
console.log(`x: ${p.x}, y: ${p.y}`);
}
printPoint({ x: 10, y: 20 });
3.4 可索引类型
// 数组类型
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = ["Bob", "Fred"];
const first: string = myArray[0];
// 字典类型
interface Dictionary {
[index: string]: string;
}
const dict: Dictionary = {
name: "Tom",
age: "25"
};
深入理解
接口 vs 类型别名
// 接口
define interface Point {
x: number;
y: number;
}
// 类型别名
type Point = {
x: number;
y: number;
};
主要区别:
| 特性 | Interface | Type |
|---|---|---|
| 扩展方式 | extends(合并声明) |
&(交叉类型) |
| 同名声明 | 自动合并(声明合并) | 报错 |
| 适用场景 | 对象结构、类实现 | 联合类型、原始类型、元组 |
| 计算属性 | 不支持 | 支持 |
// 接口的声明合并
interface Window {
myLib: any;
}
interface Window {
myApp: any;
}
// Window 同时具有 myLib 和 myApp
// 类型别名不能声明合并
type Window = { myLib: any; };
// type Window = { myApp: any; }; // Error: 重复标识符 "Window"
鸭子类型(Duck Typing)
TypeScript 使用结构类型系统,只要结构匹配,就认为类型兼容。
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
// 只要传入对象有 label 属性即可,不需要显式声明实现接口
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj); // ok
最佳实践
- 优先使用接口定义对象结构:除非需要联合类型或交叉类型
- 使用 readonly 保护数据:特别是配置对象、常量数据
- 合理使用可选属性:避免过多可选属性导致类型定义不清晰
- 利用接口继承:提取公共类型,避免重复定义
- 使用索引签名谨慎:会削弱类型检查,只在必要时使用
面试要点
- 接口定义:描述对象的结构契约,不包含实现
- 可选属性:
?标记,类型为T | undefined - 只读属性:
readonly标记,初始化后不可修改 - 函数类型:描述函数参数和返回值
- 索引签名:
[key: string]: any处理动态属性 - 接口继承:
extends支持单继承和多继承 - 声明合并:同名接口自动合并,类型别名不支持
- 与类型别名的区别:接口更适合对象结构,类型别名更灵活