Chapter 03

接口与类型别名:interface vs type

深入理解 interface 与 type 的相同与不同,掌握声明合并与映射类型的基础

TypeScript 类型系统的结构化原理

结构类型系统(Structural Type System)

TypeScript 使用结构类型系统(也称"鸭子类型"),而非名义类型系统(Nominal Type System)。这意味着:类型兼容性由类型的结构(属性形状)决定,而非类型的名称或声明来源。

结构类型系统(Structural Typing)
如果类型 A 具有类型 B 所需的所有属性,则 A 兼容 B——即使 A 和 B 从未有过显式的继承关系。TypeScript、Go 使用结构类型。例:一个有 name: string 和 age: number 的对象,可以赋值给要求 { name: string } 的变量,即使两者类型名称不同。
名义类型系统(Nominal Typing)
即使两个类型结构完全相同,如果名称不同,它们也不兼容。Java、C# 使用名义类型。在这类语言中,两个名为 Cat 和 Dog 的类,即使都有 name: string,也不能互相赋值,必须显式继承才能兼容。
多余属性检查(Excess Property Check)
TypeScript 有一个例外:将对象字面量直接赋给有类型标注的变量时,会触发多余属性检查,不允许有接口未声明的属性。但将已赋值的变量(非字面量)传递时,不触发此检查。这是结构类型系统的特殊规则,用于防止常见的拼写错误。
interface Named { name: string; }

// 结构类型兼容性:无需显式声明 implements
const user = { name: '张三', age: 28 };
const named: Named = user;  // ✅ user 有 name 属性,结构兼容

// 多余属性检查(对象字面量直接赋值时触发)
const n1: Named = { name: '李四', age: 30 };  // ❌ 多余属性 age 报错
const n2: Named = user;                         // ✅ 通过变量传递,不触发检查

// 函数参数同样适用:函数参数兼容更宽松(协变/逆变规则)
function greet(n: Named) { console.log(n.name); }
greet({ name: '王五', email: 'w@a.com' }); // ❌ 字面量触发多余属性检查
greet(user);                                   // ✅ 变量传递,不触发
常见误区:多余属性检查只针对字面量

很多开发者以为 TypeScript 不允许对象有接口未声明的属性,这是误解。TypeScript 的结构类型系统允许"超集"的赋值。多余属性检查只在对象字面量直接赋值给类型标注的位置时触发(函数调用时字面量参数、直接变量赋值等),目的是捕捉拼写错误,而非强制"精确"匹配。

interface:定义对象形状

基本 interface 语法

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;                    // 可选属性
  readonly createdAt: Date;        // 只读属性
  [key: string]: unknown;          // 索引签名(允许任意额外属性)
}

// 函数类型接口
interface Formatter {
  (value: string, options?: { uppercase?: boolean }): string;
}

// 构造函数类型接口
interface Constructor<T> {
  new (...args: any[]): T;
}

接口继承

interface Animal {
  name: string;
  age: number;
}

interface Pet extends Animal {
  owner: string;
}

// 多接口继承
interface ServiceDog extends Pet, Serializable {
  serviceType: string;
}

声明合并(Declaration Merging)

// interface 的独特特性:多次声明会自动合并
interface Window {
  myCustomProperty: string;
}
// 现在 window.myCustomProperty 是合法的(常用于扩展全局类型)

interface Config { host: string; }
interface Config { port: number; }
// 合并结果等同于:interface Config { host: string; port: number; }

type 别名

type 的用途

// 对象类型
type Point = { x: number; y: number };

// 联合类型(interface 无法做到)
type StringOrNumber = string | number;

// 元组(interface 无法做到)
type Pair<T> = [T, T];

// 从其他类型派生
type UserKeys = keyof User;  // 'id' | 'name' | 'email' | ...
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// 条件类型(interface 无法做到)
type IsString<T> = T extends string ? true : false;

interface vs type:何时用哪个?

特性 interface type
对象形状
声明合并 ✅(自动合并) ❌(不支持)
联合/交叉类型
映射类型
条件类型
implements(类实现) ✅(对象类型)
错误信息可读性 通常更简洁 复杂类型时较长
实践建议

通用原则:定义对象形状(特别是公共 API)时用 interface,因为它支持声明合并(方便第三方扩展),错误信息也更清晰;需要联合、条件、映射等高级类型时用 type。在同一个代码库中,选定一种风格并保持一致即可——两者都正确。

interface 与 type 的深层区别

声明合并(Declaration Merging)
interface 的独有特性:同名 interface 会自动合并属性。这是 TypeScript 设计用于扩展第三方库类型(如扩展 Window、Express.Request)的机制。type 别名不支持重复声明,会报"Duplicate identifier"错误。
类型别名的惰性求值
type 别名支持递归类型定义(如 JSON 类型),因为 type 是惰性求值的。interface 在某些递归场景下也支持,但行为略有不同——interface 的名称在定义内部就已绑定,type 在展开时才求值。
错误信息中的表现
interface 类型在错误信息中通常以名称显示(如 "Type 'X' is not assignable to type 'User'");复杂的 type 别名(特别是映射类型)可能被展开显示,错误信息更长更难读。对公共 API 优先用 interface,错误更友好。
// 递归类型:JSON 值的类型定义
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]                    // 递归:数组元素也是 JSONValue
  | { [key: string]: JSONValue };  // 递归:对象值也是 JSONValue

// 使用:可以类型安全地处理任意 JSON 数据
const data: JSONValue = {
  name: '张三',
  scores: [95, 87, 92],
  address: {
    city: '北京',
    zip: null
  }
};  // ✅ 深层嵌套结构

接口高级特性

索引签名(Index Signatures)

索引签名允许对象拥有任意数量的属性,只要键和值类型符合约束:

interface Dictionary<T> {
  [key: string]: T;
}
const scores: Dictionary<number> = { alice: 95, bob: 87 };

// 已知属性必须与索引签名的值类型兼容(是其子集)
interface Config {
  [key: string]: string | number;  // 索引签名允许 string 或 number
  host: string;    // ✅ string 是 string|number 的子集
  port: number;    // ✅
  // enabled: boolean  ❌ boolean 不是 string|number 的子集
}

交叉类型(Intersection Types)

type User = { id: number; name: string };
type Admin = { adminLevel: number; permissions: string[] };

// AdminUser 必须同时满足 User 和 Admin 的所有约束
type AdminUser = User & Admin;

// 实用场景:为函数参数注入额外属性
function withTimestamp<T>(data: T): T & { createdAt: Date } {
  return { ...data, createdAt: new Date() };
}

// 注意:交叉冲突的属性会产生 never 类型
type Conflict = { id: string } & { id: number };
// Conflict.id 的类型是 string & number = never
// 意味着没有任何值可以赋给 Conflict.id

扩展全局类型

// 扩展 Express Request(为 req.user 添加类型)
declare namespace Express {
  interface Request {
    user?: { id: string; email: string; role: string };
  }
}

// 扩展浏览器全局 Window
declare global {
  interface Window {
    analytics: { track: (event: string) => void };
  }
}
// 之后 window.analytics.track('page_view') 是类型安全的

本章小结

本章核心要点