接口设计与结构化类型:为业务数据建模
需求场景
订单系统的 API 文档已经定稿,你需要为以下数据结构编写 TypeScript 类型定义:
- 用户信息(必填字段 + 可选字段)
- 商品详情(含嵌套的规格参数)
- 订单记录(含状态枚举 + 支付信息)
- 分页查询响应(通用结构 + 泛型数据体)
这些数据结构将贯穿整个项目,被数十个模块引用。你需要一套精确、可扩展、易维护的类型定义方案。这就是 interface 和对象类型系统要解决的问题。
对象类型的内联定义
最直接的方式是用花括号列出属性名和类型:
const product: {
sku: string;
title: string;
price: number;
} = {
sku: "PHONE-256G",
title: "Smartphone 256GB",
price: 3999,
};
属性之间可以用分号或逗号分隔。对象在直接赋值时必须严格匹配——多属性或少属性都会报错:
// 少属性
const item: { sku: string; price: number } = {
sku: "TAB-128",
// 编译错误:缺少 price
};
// 多属性
const item2: { sku: string; price: number } = {
sku: "TAB-128",
price: 2999,
color: "black", // 编译错误:对象字面量不允许额外属性
};
但通过中间变量赋值时,TypeScript 采用结构化类型匹配(structural typing),只要结构兼容就通过:
const temp = { sku: "TAB-128", price: 2999, color: "black" };
const item3: { sku: string; price: number } = temp; // 通过
这就是 TypeScript 的核心类型哲学:关注结构是否匹配,而非名称是否一致。只要一个对象具备了所需的全部属性且类型正确,就认为它满足类型要求。
可选属性
业务数据中经常有「可填可不填」的字段。用 ? 标记可选属性:
interface CustomerInfo {
customerId: string;
fullName: string;
phone: string;
wechat?: string; // 可选
remark?: string; // 可选
}
const buyer: CustomerInfo = {
customerId: "C-10001",
fullName: "Li Ming",
phone: "13800138000",
};
// wechat 和 remark 可以不传
可选属性的实际类型是 声明类型 | undefined。使用前建议做判空:
function sendNotification(customer: CustomerInfo): void {
if (customer.wechat) {
console.log(`WeChat message to ${customer.wechat}`);
} else {
console.log(`SMS to ${customer.phone}`);
}
}
可选属性和「值为 undefined 的必填属性」是不同的概念:
interface OptionalField {
memo?: string; // 可以不传这个字段
}
interface UndefinedField {
memo: string | undefined; // 必须传这个字段,但值可以是 undefined
}
const a: OptionalField = {}; // 通过
const b: UndefinedField = {}; // 编译错误:缺少 memo
const c: UndefinedField = { memo: undefined }; // 通过
只读属性
某些字段在创建后不应被修改,例如数据库主键、创建时间:
interface OrderRecord {
readonly orderId: string;
readonly createdAt: number;
status: string;
totalAmount: number;
}
const order: OrderRecord = {
orderId: "ORD-20260312-001",
createdAt: Date.now(),
status: "pending",
totalAmount: 599,
};
order.status = "shipped"; // 通过
order.orderId = "ORD-NEW"; // 编译错误:只读属性
readonly 仅在编译阶段生效。对于嵌套对象,readonly 只阻止重新赋值整个属性,不阻止修改内部字段:
interface AppSettings {
readonly database: {
host: string;
port: number;
};
}
const settings: AppSettings = {
database: { host: "localhost", port: 5432 }
};
settings.database.host = "192.168.1.100"; // 通过
settings.database = { host: "new", port: 3306 }; // 编译错误
索引签名
当对象的属性名不固定或数量很多时,逐一列举不现实。索引签名描述的是「属性名的模式」:
interface TranslationMap {
[key: string]: string;
}
const zhCN: TranslationMap = {
greeting: "你好",
farewell: "再见",
thanks: "谢谢",
};
索引签名的键可以是 string、number 或 symbol。一个重要约束是:已声明的具名属性的类型必须兼容索引签名的值类型:
interface ProductCatalog {
defaultCategory: string; // 合法:string 兼容索引签名的值类型
itemCount?: number; // 编译错误:number 不兼容 string
[key: string]: string;
}
解决方法是扩大索引签名的值类型:
interface ProductCatalog {
defaultCategory: string;
itemCount?: number;
[key: string]: string | number | undefined;
}
数字索引和字符串索引可以共存,但数字索引的值类型必须是字符串索引值类型的子类型:
interface HybridCollection {
[idx: number]: string;
[key: string]: string | number;
}
interface 声明
当同一个对象类型被多处引用时,用 interface 单独声明是更优的做法:
interface Warehouse {
warehouseCode: string;
location: string;
capacity: number;
isActive: boolean;
}
function displayWarehouse(wh: Warehouse): void {
console.log(`${wh.warehouseCode} @ ${wh.location}`);
}
function sortWarehouses(list: Warehouse[]): Warehouse[] {
return list.sort((a, b) => b.capacity - a.capacity);
}
接口中的方法有三种等价的声明形式:
// 简写
interface Calculator {
compute(a: number, b: number): number;
}
// 箭头函数形式
interface Calculator {
compute: (a: number, b: number) => number;
}
// 对象字面量形式
interface Calculator {
compute: { (a: number, b: number): number };
}
接口还可以直接描述函数类型和构造函数类型:
interface MatchFn {
(text: string, pattern: string): boolean;
}
const regexMatch: MatchFn = (text, pattern) => {
return new RegExp(pattern).test(text);
};
interface WidgetConstructor {
new (config: { width: number; height: number }): Widget;
}
接口继承
接口通过 extends 继承其他接口,实现类型的组合与复用:
interface Timestamped {
createdAt: number;
updatedAt: number;
}
interface SoftDeletable {
deletedAt?: number;
isDeleted: boolean;
}
interface BaseEntity extends Timestamped, SoftDeletable {
entityId: string;
}
// BaseEntity 同时拥有 Timestamped、SoftDeletable 和自身的全部属性
const record: BaseEntity = {
entityId: "E-001",
createdAt: Date.now(),
updatedAt: Date.now(),
isDeleted: false,
};
接口不仅可以继承接口,还可以继承 type 定义的类型:
type GeoLocation = {
latitude: number;
longitude: number;
};
interface Store extends GeoLocation {
storeName: string;
openHours: string;
}
甚至可以继承类,此时接口继承的是类的实例属性和方法类型:
class Coordinate {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
interface Coordinate3D extends Coordinate {
z: number;
}
const point: Coordinate3D = { x: 1, y: 2, z: 3 };
声明合并
TypeScript 的一个独特能力:同名接口会自动合并。这在扩展第三方库的类型时非常有用:
interface ShipmentInfo {
trackingNo: string;
carrier: string;
}
interface ShipmentInfo {
estimatedArrival: string;
}
// 等价于:
// interface ShipmentInfo {
// trackingNo: string;
// carrier: string;
// estimatedArrival: string;
// }
const shipment: ShipmentInfo = {
trackingNo: "SF1234567890",
carrier: "SF Express",
estimatedArrival: "2026-03-15",
};
合并规则:
同名属性类型必须一致:
interface Config {
timeout: number;
}
interface Config {
timeout: string; // 编译错误:与前面的声明冲突
}
同名方法形成函数重载,后声明的优先级更高,但字面量类型参数总是最高优先级:
interface EventSystem {
dispatch(action: string): void;
}
interface EventSystem {
dispatch(action: "reset"): boolean;
dispatch(action: "save"): boolean;
}
// 合并后重载顺序:
// dispatch(action: "reset"): boolean;
// dispatch(action: "save"): boolean;
// dispatch(action: string): void;
声明合并最常见的实际用途是补充全局类型:
interface Window {
__APP_VERSION__: string;
__DEBUG_MODE__: boolean;
}
window.__APP_VERSION__ = "2.1.0";
window.__DEBUG_MODE__ = false;
type 与 interface 的选择
两者都可以描述对象的结构,但有关键差异。
共同能力
// interface 描述对象
interface ProductA {
sku: string;
price: number;
}
// type 描述对象
type ProductB = {
sku: string;
price: number;
};
两者都支持扩展,语法不同:
// interface 用 extends
interface DetailedProductA extends ProductA {
specs: string[];
}
// type 用交叉类型
type DetailedProductB = ProductB & {
specs: string[];
};
核心差异
| 能力 | interface | type |
|---|---|---|
| 描述非对象类型 | 不支持 | 支持 |
| 声明合并 | 支持 | 不支持 |
| 联合类型 | 不支持 | 支持 |
| 映射类型 | 不支持 | 支持 |
| 条件类型 | 不支持 | 支持 |
// type 独有的能力
type OrderState = "pending" | "paid" | "shipped" | "completed";
type Pair = [string, number];
type ReadOnly<T> = { readonly [K in keyof T]: T[K] };
type IsArray<T> = T extends any[] ? true : false;
选择建议
- 定义对象结构(特别是类需要
implements的契约)时,优先用interface - 定义联合类型、元组、条件类型、映射类型时,用
type
一句话:interface 是开放的(可以合并扩展),type 是封闭的(定义即固定)。
嵌套对象类型
实际业务数据通常是多层嵌套的:
interface Province {
provinceName: string;
provinceCode: string;
}
interface City {
cityName: string;
cityCode: string;
province: Province;
}
interface DeliveryAddress {
addressId: string;
receiver: string;
phone: string;
city: City;
street: string;
postalCode: string;
isDefault?: boolean;
}
const addr: DeliveryAddress = {
addressId: "ADDR-001",
receiver: "Zhang Wei",
phone: "13900139000",
city: {
cityName: "Hangzhou",
cityCode: "330100",
province: {
provinceName: "Zhejiang",
provinceCode: "330000",
},
},
street: "No.1 Westlake Road",
postalCode: "310000",
isDefault: true,
};
对于递归结构(如评论树、组织架构),接口可以引用自身:
interface CommentNode {
commentId: string;
content: string;
author: string;
replies: CommentNode[];
}
const thread: CommentNode = {
commentId: "C-1",
content: "Great product!",
author: "user-a",
replies: [
{
commentId: "C-2",
content: "Thanks!",
author: "seller",
replies: [],
},
],
};
综合实战:定义分页 API 响应
将本章知识串联,为订单系统定义一套完整的 API 响应类型:
interface PaginationMeta {
currentPage: number;
pageSize: number;
totalRecords: number;
totalPages: number;
}
interface ApiEnvelope<T> {
code: number;
msg: string;
timestamp: number;
payload: {
records: T[];
pagination: PaginationMeta;
};
}
interface OrderSummary {
readonly orderId: string;
customerName: string;
totalAmount: number;
status: "pending" | "paid" | "shipped" | "completed" | "cancelled";
payment?: {
method: "alipay" | "wechat" | "card";
transactionId?: string;
};
}
type OrderListResponse = ApiEnvelope<OrderSummary>;
async function fetchOrders(page: number): Promise<OrderListResponse> {
const resp = await fetch(`/api/orders?page=${page}`);
return resp.json();
}
这个设计综合使用了:接口、可选属性、只读属性、嵌套对象、泛型(后续章节详解)、联合字面量类型。ApiEnvelope<T> 是通用的分页响应容器,传入不同的业务类型参数即可生成对应的响应类型,极大提升了复用性。
本章回顾
| 概念 | 说明 | 关键语法 |
|---|---|---|
| 对象类型 | 用花括号列出属性及类型 | { key: Type } |
| 可选属性 | 字段可以缺省 | key?: Type |
| 只读属性 | 初始化后不可修改 | readonly key: Type |
| 索引签名 | 描述动态属性名 | [key: string]: Type |
| interface | 为对象类型命名,支持合并 | interface Name { } |
| 接口继承 | 从已有接口派生 | interface B extends A |
| 声明合并 | 同名接口自动合并 | 多次声明同名 interface |
| type vs interface | type 更灵活,interface 更语义化 | 对象优先 interface |
| 嵌套对象 | 属性本身也是对象类型 | 接口组合或内联定义 |
| 结构化类型 | 只要结构匹配即类型兼容 | TypeScript 核心理念 |
| 递归类型 | 接口引用自身 | 适用于树形数据 |
购买课程解锁全部内容
告别类型错误:12 章掌握 TypeScript 工程实战
¥29.90