跳到主要内容
预计阅读 26 分钟

接口设计与结构化类型:为业务数据建模

需求场景

订单系统的 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: "谢谢",
};

索引签名的键可以是 stringnumbersymbol。一个重要约束是:已声明的具名属性的类型必须兼容索引签名的值类型

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[];
};

核心差异

能力interfacetype
描述非对象类型不支持支持
声明合并支持不支持
联合类型不支持支持
映射类型不支持支持
条件类型不支持支持
// 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 interfacetype 更灵活,interface 更语义化对象优先 interface
嵌套对象属性本身也是对象类型接口组合或内联定义
结构化类型只要结构匹配即类型兼容TypeScript 核心理念
递归类型接口引用自身适用于树形数据

购买课程解锁全部内容

告别类型错误:12 章掌握 TypeScript 工程实战

¥29.90