TypeScript 类型系统

TypeScript 的类型系统是它区别于 JavaScript 的根本特征。理解它的设计哲学——结构化类型(Structural Typing)——是理解 TypeScript 一切类型行为的关键。

跟 Python 的鸭子类型(“有这个方法就能用”)相比,TS 就是”编译期强制的鸭子类型”——形状对得上就兼容,但 tsc 不通过你根本跑不起来。


设计哲学:为什么是结构化类型

与 JS 生态的共生关系

TypeScript 的目标不是重新发明一门语言,而是为已有的 JavaScript 生态加类型注释。十亿行 JS 代码不会为 TS 重写,所以 TS 的类型系统必须容忍 JS 的灵活性。

// 没有任何 interface 声明,纯 JS 对象也能通过类型检查
const user = { name: "Alice", age: 30, email: "[email protected]" };
 
function greet(p: { name: string; age: number }) {
  return `Hello, ${p.name}`;
}
 
greet(user); // 通过 — user 的形状兼容参数类型

Python 鸭子类型 → TS 结构化类型

TS 的结构化类型本质上是 Python 鸭子类型在静态类型系统中的自然延伸:

# Python:鸭子类型 — 运行时只要对象有 .x 和 .y 就能用
def distance(p):
    return (p.x ** 2 + p.y ** 2) ** 0.5
 
# 任何有 x、y 属性的对象都能传进去
class Point:
    def __init__(self, x, y): self.x, self.y = x, y
 
class Vec3:
    def __init__(self, x, y, z): self.x, self.y, self.z = x, y, z
 
distance(Point(3, 4))  #
distance(Vec3(3, 4, 5)) # 多了 .z 也无所谓
// TS:结构化类型 — 编译期做同样的事,但不通过编译就不让运行
interface HasXY {
  x: number;
  y: number;
}
 
function distance(p: HasXY): number {
  return Math.sqrt(p.x ** 2 + p.y ** 2);
}
 
const pt = { x: 3, y: 4, z: 5 };
distance(pt); // 编译通过 — pt 至少包含 x、y

Python 里类型错误在运行时爆;TS 里 tsc 不通过你根本跑不起来。这是”灵活的鸭子”变成”有门禁的鸭子”。

结构化类型 vs 标称类型

维度结构化类型 (TS)鸭子类型 (Python)标称类型 (Java/C#)
兼容判断形状匹配即可有方法/属性就用显式声明 implements 或继承
检查时机编译期(强制)运行时(崩溃才知)编译期(强制)
灵活性极高
代表场景JSON 响应、配置对象、API快速实验、脚本领域模型、微服务边界
// TS:多余字段不阻碍赋值(结构兼容)
interface Point {
  x: number;
  y: number;
}
 
const p3 = { x: 1, y: 2, z: 3 };
const pt: Point = p3; // 通过 — p3 至少包含 x、y
 
// Java 则需要:
// class MyPoint implements Point { ... }

代价:两个结构相同但语义不同的类型(如 UserId vs ProductId)会被视为兼容。这要靠”品牌化”技巧来模拟名义判断:

type UserId = string & { readonly __brand: "UserId" };
type ProductId = string & { readonly __brand: "ProductId" };
 
function getUser(id: UserId) { /* ... */ }
 
const uid = "abc" as UserId;
getUser(uid); // 通过
// getUser("abc"); // 类型错误 — 普通 string 不兼容

核心类型工具

interface vs type:选择边界

维度interfacetype
声明合并✅ 同名自动合并❌ 报错
扩展extends& 交叉
基本类型别名❌ 不支持type Name = string
联合/元组❌ 不支持type A = string | number
性能(大项目)更优(缓存)略差(每次评估)
社区建议对象形状优先用联合/映射/条件类型用
// 对象形状 → interface
interface User {
  name: string;
  age: number;
}
 
// 联合类型 → type
type Status = "idle" | "loading" | "success" | "error";
 
// 函数签名 → type 更简洁
type FetchFn = (url: string) => Promise<Response>;
 
// 交叉类型扩展
type Admin = User & { role: "admin"; permissions: string[] };

联合类型与交叉类型

TS 的联合类型(|)是表达能力最强的特性之一,远超大多数语言的枚举:

// 可辨识联合(Discriminated Union)— TS 的"ADT"
type Result<T> =
  | { status: "ok"; data: T }
  | { status: "error"; message: string };
 
function handle<T>(result: Result<T>) {
  switch (result.status) {
    case "ok":
      return result.data;    // TS 自动收窄类型
    case "error":
      return result.message; // 同上
  }
}

交叉类型(&)合并多个类型的所有属性:

type A = { a: string };
type B = { b: number };
type C = A & B; // { a: string; b: number }

泛型:约束而非放任

// 基础泛型
function first<T>(arr: T[]): T {
  return arr[0];
}
 
// 泛型约束 — 要求 T 有 length 属性
function countField<T extends { length: number }>(item: T): number {
  return item.length;
}
 
countField("hello");  // 5
countField([1, 2]);   // 2
// countField(42);    // number 没有 length
 
// keyof + 索引访问 — 类型安全的属性访问
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // 返回 string
// getProperty(user, "email"); // 编译错误

satisfies 运算符:既约束又保留

TS 4.9 引入的 satisfies 解决了”用 as 太粗暴,用注解丢失精度”的困境:

// 普通注解 — 丢失字面量精度
const config: Record<string, string | number> = {
  width: 800,     // 类型收窄为 number,丢掉了 800
  title: "Hello", // 类型收窄为 string
};
config.width.toFixed(); // 报错 — number 没有 toFixed
 
// satisfies — 约束形状 + 保留细粒度推断
const config2 = {
  width: 800,
  title: "Hello",
} satisfies Record<string, string | number>;
 
config2.width;   // 类型是 800(字面量),不是 number
config2.title;   // 类型是 "Hello"

as constconst 类型参数

// as const — 冻结为最窄类型
const routes = ["/home", "/about", "/contact"] as const;
// 类型:readonly ["/home", "/about", "/contact"]
 
// const 类型参数 (TS 5.0+)
function useConfig<const T>(config: T) {
  return config;
}
 
const c = useConfig({ theme: "dark" });
c.theme; // 类型是 "dark",而非 string

高级类型体操:边界与克制

三大核心工具

// 条件类型
type IsString<T> = T extends string ? true : false;
 
// 映射类型 — 基于已有类型生成新类型
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
 
// 模板字面量类型 — 字符串级别的类型运算
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type Route = `/api/${string}`;

类型体操的边界

社区在 2024–2026 年形成了”健康用法共识”:

适合类型体操不适合类型体操
库/框架的公共 API(如 Zod、tRPC)业务组件内部逻辑
类型层面的代码生成(如根据路由推导参数)能用 if/switch 表达的运行时逻辑
一次性封装的工具类型超过 2 层嵌套的条件类型

核心原则:类型系统是”约束”工具,不是”证明”工具。 把复杂校验交给 Zod/Valibot 等运行时 schema 库,TS 类型只描述结构:

import { z } from "zod";
 
// 运行时校验 + 自动推导 TS 类型
const UserSchema = z.object({
  name: z.string(),
  age: z.number().min(0).max(150),
});
 
type User = z.infer<typeof UserSchema>; // 自动从 schema 推导类型

与 Python 的对比

维度TypeScriptPython
类型哲学结构化类型 — “形状对得上就行”鸭子类型 — “有这个方法就行”
类型检查编译期强制(tsc运行时 + 可选静态(mypy/Pyright)
类型擦除✅ 编译后 JS 不携带类型信息✅ 解释器忽略注解
联合类型✅ 一等公民 A | B⚠️ Union[A, B],不支持可辨识联合
条件类型T extends U ? X : Y❌ 不支持
泛型体验流畅(<T> 内联)改善中(PEP 695 后简化,仍冗长)
any 逃生门✅ 存在,不应滥用⚠️ 默认无类型约束

Python 类型标注的干扰点:你习惯的 Pydantic BaseModel 在运行时保留类型信息并做校验;TS 的类型在运行时完全消失,必须用 Zod/Valibot 做”类型擦除后的校验”。这是 Python → TS 最常见的认知落差。


any vs unknown vs never

// any — 关闭类型检查(逃生门,尽量少用)
let x: any = 42;
x.foo.bar(); // 编译不报错,运行时才崩
 
// unknown — 安全的"不知道类型"
let y: unknown = 42;
// y.foo();  // 编译报错 — unknown 不能直接操作
if (typeof y === "string") {
  y.toUpperCase(); // 收窄后可用
}
 
// never — 永远不会发生的类型
function throwError(msg: string): never {
  throw new Error(msg);
}
type NonNullable<T> = T extends null | undefined ? never : T;

Python 视角any 相当于 Python 的裸变量(无类型约束),unknown 相当于 object(必须先 isinstance 收窄),never 在 Python 中没有直接对应。


关联