跳转至

第 2 章:类型系统核心

学习时间:4 小时 | 难度:⭐⭐ 初级 | 前置知识:第 1 章


本章概览

TypeScript 的灵魂是类型系统。本章深入讲解接口(Interface)、类型别名(Type Alias)、枚举(Enum)、联合类型与交叉类型,以及类型守卫——这些是每个 TS 开发者每天都在使用的核心工具。

学习目标:

  • 掌握 interfacetype 的区别与使用场景
  • 理解联合类型(|)与交叉类型(&)的语义
  • 灵活运用字面量类型和辨识联合(Discriminated Union)
  • 掌握所有类型守卫:typeofinstanceofin、自定义谓词
  • 理解枚举的利弊,知道何时用常量枚举替代
  • 掌握 TypeScript 的结构化类型系统(鸭子类型)

2.1 Interface(接口)

接口是 TypeScript 描述对象形状(shape)的主要方式:

TypeScript
// 基本接口定义
interface User {
  readonly id: number;        // readonly:创建后不可修改
  name: string;
  email: string;
  age?: number;               // ?: 表示可选属性
  createdAt: Date;
}

// 接口的可选属性和函数签名
interface Repository<T> {
  findById(id: number): Promise<T | null>;
  findAll(options?: QueryOptions): Promise<T[]>;
  save(entity: T): Promise<T>;
  delete(id: number): Promise<void>;
}

interface QueryOptions {
  limit?: number;
  offset?: number;
  orderBy?: string;
}

// 接口继承:extends 可以继承多个接口
interface AdminUser extends User {
  role: "admin";
  permissions: string[];
}

// 接口合并(Declaration Merging):同名接口自动合并
// 常用于扩展第三方库的类型
interface Window {
  myCustomProperty: string; // 给全局 Window 对象添加类型
}

// 索引签名:描述动态键的对象
interface StringMap {
  [key: string]: string; // 任意字符串键,任意字符串值
}

const headers: StringMap = {
  "Content-Type": "application/json",
  Authorization: "Bearer token123",
};

// 可调用接口(描述函数)
interface Formatter {
  (value: number, locale?: string): string; // 调用签名
  defaultLocale: string;                    // 静态属性
}

接口 vs 类型别名:何时用哪个?

TypeScript
// Interface 适合:
// 1. 描述对象/类的形状(可扩展、可合并)
// 2. 面向对象风格(implements)
// 3. 定义公共 API(库/SDK 开发)

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// Type Alias 适合:
// 1. 联合类型、交叉类型
// 2. 工具类型(Utility Types)
// 3. 元组
// 4. 任何 interface 无法表达的类型

type ID = string | number;
type Status = "pending" | "active" | "inactive";
type Pair<T> = [T, T];
type Nullable<T> = T | null;

2.2 联合类型(Union Types)

联合类型 A | B 表示值可以是 A B:

TypeScript
// 基础联合类型
type StringOrNumber = string | number;
type NullableString = string | null;

function formatId(id: string | number): string {
  // 在使用前必须缩窄类型
  if (typeof id === "number") {
    return id.toString().padStart(8, "0"); // 这里 id 是 number
  }
  return id.toUpperCase(); // 这里 id 是 string
}

// 辨识联合(Discriminated Union)— TypeScript 最强大的模式之一
// 每个成员都有一个公共的字面量类型字段("辨识符")
type Shape =
  | { kind: "circle";    radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle";  base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // 这里 shape 是 { kind: "circle"; radius: number }

    case "rectangle":
      return shape.width * shape.height;  // 这里 shape 是 rectangle 类型

    case "triangle":
      return (shape.base * shape.height) / 2;

    // 如果新增 Shape 成员但忘记处理,TypeScript 会报错(配合 noImplicitReturns)
    default:
      const _exhaustive: never = shape; // 穷举检查
      throw new Error(`Unknown shape: ${_exhaustive}`);
  }
}

console.log(area({ kind: "circle", radius: 5 }));       // 78.54
console.log(area({ kind: "rectangle", width: 4, height: 3 })); // 12

// 实际应用:API 响应类型
type ApiResult<T> =
  | { success: true;  data: T }
  | { success: false; error: string; code: number };

function handleResult<T>(result: ApiResult<T>): T {
  if (result.success) {
    return result.data; // 这里 TS 知道 result 有 data
  }
  // 这里 TS 知道 result 有 error 和 code
  throw new Error(`[${result.code}] ${result.error}`);
}

2.3 交叉类型(Intersection Types)

交叉类型 A & B 表示值同时满足 A B:

TypeScript
// 交叉类型:合并多个类型的属性
interface HasTimestamps {
  createdAt: Date;
  updatedAt: Date;
}

interface HasId {
  id: string;
}

// 组合出一个带时间戳和 ID 的类型
type BaseEntity = HasId & HasTimestamps;

interface Product extends HasId, HasTimestamps {
  name: string;
  price: number;
}

// 等价于:
type ProductType = HasId & HasTimestamps & {
  name: string;
  price: number;
};

// 常用于 Mixin 模式(多重行为组合)
type Serializable = {
  serialize(): string;
  deserialize(data: string): void;
};

type Loggable = {
  log(message: string): void;
};

type EnhancedService = Serializable & Loggable & {
  name: string;
};

// 交叉 & 联合的优先级:& 高于 |
type A = string & number;      // never(没有值同时是 string 和 number)
type B = (string | number) & string; // string(string 是 string,number 并非 string)

2.4 类型守卫(Type Guards)

类型守卫是运行时检查,让 TypeScript 在特定代码块中缩窄类型:

TypeScript
// ── 1. typeof 守卫(原始类型) ──────────────────────
function processValue(val: string | number | boolean) {
  if (typeof val === "string") {
    return val.toUpperCase(); // val: string
  } else if (typeof val === "number") {
    return val.toFixed(2);   // val: number
  } else {
    return val ? "yes" : "no"; // val: boolean
  }
}

// ── 2. instanceof 守卫(类实例) ────────────────────
class HttpError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = "HttpError";
  }
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = "ValidationError";
  }
}

function handleError(err: unknown): string {
  if (err instanceof HttpError) {
    return `HTTP ${err.statusCode}: ${err.message}`; // err: HttpError
  }
  if (err instanceof ValidationError) {
    return `Validation error on '${err.field}': ${err.message}`; // err: ValidationError
  }
  if (err instanceof Error) {
    return err.message; // err: Error
  }
  return String(err);
}

// ── 3. in 守卫(检查属性存在) ─────────────────────
interface Cat { meow(): void }
interface Dog { bark(): void }

function makeNoise(animal: Cat | Dog) {
  if ("meow" in animal) {
    animal.meow(); // animal: Cat
  } else {
    animal.bark(); // animal: Dog
  }
}

// ── 4. 自定义类型谓词(is 关键字) ─────────────────
// 返回类型 "param is Type" 告诉 TypeScript:如果函数返回 true,param 就是 Type

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    "email" in value
  );
}

interface User { id: number; name: string; email: string }

// 在条件分支后,TypeScript 自动缩窄类型
const data: unknown = JSON.parse('{"id": 1, "name": "Alice", "email": "a@b.com"}');
if (isUser(data)) {
  console.log(data.name); // data: User — 有完整类型支持
}

// ── 5. 断言函数(asserts) ──────────────────────────
// 如果函数正常返回(不抛出异常),则断言成立
function assertIsString(val: unknown): asserts val is string {
  if (typeof val !== "string") {
    throw new TypeError(`Expected string, got ${typeof val}`);
  }
}

const input: unknown = "hello";
assertIsString(input); // 如果这里没抛出,下面 input 就是 string
console.log(input.toUpperCase()); // OK:input: string

2.5 枚举(Enum)

TypeScript
// ── 数字枚举(默认从 0 开始自增) ─────────────────
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right, // 3
}
console.log(Direction.Up);    // 0
console.log(Direction[0]);    // "Up"(反向映射,字符串枚举无此特性)

// ── 字符串枚举(推荐:可读性更好,序列化友好) ────
enum Status {
  Pending  = "PENDING",
  Active   = "ACTIVE",
  Inactive = "INACTIVE",
}
console.log(Status.Active); // "ACTIVE"

// ── const enum(零运行时开销,编译后直接内联值) ──
const enum HttpMethod {
  GET    = "GET",
  POST   = "POST",
  PUT    = "PUT",
  DELETE = "DELETE",
}
// 编译后:const method = "GET"; (枚举对象不会生成)
const method = HttpMethod.GET;

// ── 枚举的替代方案(现代 TS 风格更推荐)────────────
// 对于简单的字符串枚举,使用 as const 对象更灵活:
const HTTP_STATUS = {
  OK:           200,
  CREATED:      201,
  BAD_REQUEST:  400,
  UNAUTHORIZED: 401,
  NOT_FOUND:    404,
  SERVER_ERROR: 500,
} as const;

// 从 as const 对象提取值类型
type HttpStatusCode = typeof HTTP_STATUS[keyof typeof HTTP_STATUS];
// HttpStatusCode = 200 | 201 | 400 | 401 | 404 | 500

function handleStatus(code: HttpStatusCode) {
  if (code === HTTP_STATUS.OK) {
    console.log("Success!");
  }
}

2.6 结构化类型系统(鸭子类型)

TypeScript 使用结构化类型系统:只要形状匹配,类型就兼容。

TypeScript
interface Point2D {
  x: number;
  y: number;
}

interface Point3D {
  x: number;
  y: number;
  z: number; // 额外属性
}

function logPoint(point: Point2D) {
  console.log(`(${point.x}, ${point.y})`);
}

const p3d: Point3D = { x: 1, y: 2, z: 3 };
logPoint(p3d); // ✅ OK!Point3D 有 Point2D 所有的属性,兼容

// 对象字面量有额外检查(Excess Property Check)
// logPoint({ x: 1, y: 2, z: 3 }); // ❌ 错误:对象字面量不能有多余属性
// 但通过变量传递就没有这个检查(如上面的 p3d)

// 函数类型的结构化兼容
type BinaryFn = (a: number, b: number) => number;

const add: BinaryFn = (a, b) => a + b;
// 参数少的函数可以赋值给参数多的类型(参数兼容性是逆变的)
const addWithExtra = (a: number, b: number, _label: string) => a + b;
// addWithExtra 不能赋值给 BinaryFn(参数多于 BinaryFn)

// 但回调函数可以省略参数(这是 JavaScript 的惯例)
[1, 2, 3].forEach((item) => console.log(item)); // 只用了第一个参数,OK

2.7 实用工具类型(Built-in Utility Types)

TypeScript
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Partial<T>:所有属性变可选
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

// Required<T>:所有属性变必须(去掉 ?)
type StrictUser = Required<UserUpdate>; // 等价于 User

// Readonly<T>:所有属性变只读
type ImmutableUser = Readonly<User>;
// 常用于不可变数据结构

// Pick<T, K>:只保留指定属性
type UserPublic = Pick<User, "id" | "name" | "email">;
// 去掉 password(不对外暴露)

// Omit<T, K>:去掉指定属性(Pick 的反面)
type UserWithoutPassword = Omit<User, "password">;
// 去掉 password,保留其余所有字段

// Record<K, V>:键类型为 K,值类型为 V 的对象
type UserMap = Record<number, User>;          // 以 id 为键的用户映射
type RouteConfig = Record<string, () => void>; // 路由表

// Exclude<T, U>:从联合类型 T 中排除 U
type NotNull<T> = Exclude<T, null | undefined>; // 排除 null 和 undefined

// Extract<T, U>:从联合类型 T 中提取 U
type StringOnly = Extract<string | number | boolean, string>; // string

// NonNullable<T>:去掉 null 和 undefined
type Safe<T> = NonNullable<T>;

// ReturnType<T>:函数返回值类型
function getUser() { return { id: 1, name: "Alice" }; }
type GetUserResult = ReturnType<typeof getUser>; // { id: number; name: string }

// Parameters<T>:函数参数类型的元组
type GetUserParams = Parameters<typeof getUser>; // [](无参数)

// Awaited<T>:展开 Promise 的类型
type UserPromise = Promise<User>;
type ResolvedUser = Awaited<UserPromise>; // User

// 实际使用示例:
async function updateUser(
  id: number,
  updates: Partial<Omit<User, "id" | "createdAt">> // 不允许修改 id 和创建时间
): Promise<User> {
  // ... 更新逻辑
  return { id, name: "Alice", email: "a@b.com", password: "hash", createdAt: new Date() };
}

🏋️ 本章练习

  1. 辨识联合:为一个支付系统设计类型,包含 credit_cardpaypalcrypto 三种支付方式,每种有不同字段,写 processPayment 函数处理所有情况
  2. 类型守卫:写一个 parseJson<T> 函数,使用自定义类型谓词安全解析 JSON
  3. 工具类型组合:使用 PickOmitPartialRequired 为一个博客文章系统设计 CreatePostDtoUpdatePostDtoPostResponse 三个类型

📌 本章小结

概念 关键点
Interface 对象形状描述,支持继承和合并,面向对象场景首选
Type Alias 联合/交叉/工具类型,表达力更强
联合类型 A \| B,辨识联合是最强大的模式
交叉类型 A & B,合并属性,用于 Mixin
类型守卫 typeof/instanceof/in/is/asserts
枚举 字符串枚举可读,as const 更灵活
结构化类型 形状匹配即兼容,不看名字
工具类型 Partial/Required/Readonly/Pick/Omit/Record

下一章:函数重载、类、装饰器与 TypeScript 5.x 新特性。


TypeScript 5.8 · 2025