01. interface vs type——用哪个
interface 和 type 都能描述对象形状,大部分情况下可以互换。但在 TS 的哲学里它们有分工:
interface——用于定义对象的结构,可以被扩展(extends)、可以被同名的 interface 合并(declaration merging)。适合公开的 API 类型、第三方库的类型扩展。
type——用于类型别名,可以是任意类型(联合、交叉、函数、基本类型的别名)。不能同名的 type(报错),但可以用交叉类型 & 组合。
选 interface 还是 type:
- 需要被外部扩展(如某个库的类型让用户补字段)→ interface
- 需要联合类型、映射类型这类复杂类型 → type
- 只是一个普通对象类型 → 两者都可以,团队内部统一风格更重要
typescript
// interface——对象结构
interface User {
id: number;
name: string;
}
// type——任何类型
type ID = string | number;
type Point = { x: number; y: number };
type Callback = (data: User) => void;
// interface 可以被多次声明(合并)
interface Config {
url: string;
}
interface Config {
timeout: number; // 自动合并
}
// Config = { url: string; timeout: number }02. interface 的继承与扩展
interface 使用 extends 继承另一个 interface。可以被多个接口共同继承(多重继承)。
extends 后面可以接多个 interface 用逗号分隔:interface Child extends Parent1, Parent2。
interface 也可以 extends 一个 type(只要这个 type 是对象类型)。同样 type 可以通过交叉类型 & 跟 interface 组合。
实际项目中常见场景:基础 interface 定义核心字段,子 interface 扩展额外字段。比如 BaseEntity(id、createdAt、updatedAt)被所有实体 interface 继承。
typescript
// 基础实体
interface BaseEntity {
id: number;
createdAt: Date;
updatedAt: Date;
}
// 继承
interface User extends BaseEntity {
name: string;
email: string;
}
// 多重继承
interface Timestamped { createdAt: Date; updatedAt: Date; }
interface SoftDeletable { deletedAt?: Date; }
interface Post extends Timestamped, SoftDeletable {
id: number;
title: string;
}
// interface extends type
type WithId = { id: number };
interface Product extends WithId {
name: string;
}03. 可选属性、只读属性与索引签名
可选属性——属性名后加 ?,表示这个字段可能不存在。常用于 API 的 PATCH 请求参数(只有要改的字段才传)。
只读属性——readonly 前缀,初始化后不能修改。跟 const 的区别:const 用于变量,readonly 用于属性。
索引签名——定义未知键名的属性类型。{ [key: string]: any } 表示这个对象可以有任意字符串键。
Record<K, V> 是索引签名的泛型版——Record<string, User> 等价于 { [key: string]: User }。
泛型索引签名——{ [K in keyof T]: T[K] } 遍历 T 的所有属性创建新类型,这是映射类型的本质。
typescript
// 可选属性
interface UpdateUserDTO {
name?: string;
email?: string;
// 只传要改的字段,不改的不用传
}
// 只读属性
interface Config {
readonly apiUrl: string;
readonly timeout: number;
}
// 索引签名
interface Dictionary<T> {
[key: string]: T;
}
get users: Dictionary<User> = {};
users['user-1'] = { id: 1, name: 'Alice' };索引签名配合 readonly 可以做只读字典:{ readonly [key: string]: Value }。这在做配置对象映射时很有用。
04. 函数类型与 call signature
interface 不仅能描述对象,还能描述函数——这叫 call signature。语法:(参数列表): 返回值类型。
既有属性又能调用的对象——函数本身也是对象可以有属性,用 interface 同时描述函数的调用签名和属性。
函数类型还可以用 type 定义:type Fn = (x: number) => string。type 更简洁适合简单函数,interface 适合复杂函数(有重载、有属性)。
typescript
// call signature
interface Greeting {
(name: string): string;
}
const greet: Greeting = (name) => 'Hello, ' + name;
// 既有属性又能调用的对象
interface Logger {
(message: string): void;
level: 'info' | 'warn' | 'error';
}
const logger: Logger = (msg) => console.log(msg);
logger.level = 'info';
logger('test'); // 可以调用
// 函数重载的 interface
get OverloadedFn = {
(x: string): string;
(x: number): number;
};函数类型用 type 更常见:type Handler = (req: Request, res: Response) => void。interface 的函数签名在需要重载或带属性时更合适。
05. 接口设计最佳实践
1. 接口应该小而专注——Interface Segregation(接口隔离原则)。一个大接口拆成多个小接口再组合。
2. 使用可选属性表示「可能没有」的字段而不是 null | undefined。? 比 | undefined 更清晰。
3. 用 extends 而不是重复声明相同字段。多个接口有公共字段应该提取基接口。
4. 对外暴露的接口和内部使用的接口分开。API 返回给前端的跟数据库实体通常不同。
5. 接口文件名——很多项目用 .type.ts 或 .interface.ts 或直接在 entity 文件里定义。按团队约定统一。
typescript
// 好的做法:小接口组合
interface HasId { id: number; }
interface HasTimestamps { createdAt: Date; updatedAt: Date; }
interface HasName { name: string; }
interface User extends HasId, HasTimestamps, HasName {
email: string;
}
// API 返回给前端 vs 数据库实体
interface UserEntity extends HasId, HasTimestamps {
name: string;
passwordHash: string; // 数据库有
}
interface UserResponse {
id: number;
name: string;
// 没有 passwordHash——不给前端
}知识测验
第 1/5 题正确 0
interface 和 type 什么时候必须用 interface?
下一节
下一节 泛型