第 2 章:类型系统核心¶
学习时间:4 小时 | 难度:⭐⭐ 初级 | 前置知识:第 1 章
本章概览¶
TypeScript 的灵魂是类型系统。本章深入讲解接口(Interface)、类型别名(Type Alias)、枚举(Enum)、联合类型与交叉类型,以及类型守卫——这些是每个 TS 开发者每天都在使用的核心工具。
学习目标:
- 掌握
interface与type的区别与使用场景 - 理解联合类型(
|)与交叉类型(&)的语义 - 灵活运用字面量类型和辨识联合(Discriminated Union)
- 掌握所有类型守卫:
typeof、instanceof、in、自定义谓词 - 理解枚举的利弊,知道何时用常量枚举替代
- 掌握 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() };
}
🏋️ 本章练习¶
- 辨识联合:为一个支付系统设计类型,包含
credit_card、paypal、crypto三种支付方式,每种有不同字段,写processPayment函数处理所有情况 - 类型守卫:写一个
parseJson<T>函数,使用自定义类型谓词安全解析 JSON - 工具类型组合:使用
Pick、Omit、Partial、Required为一个博客文章系统设计CreatePostDto、UpdatePostDto、PostResponse三个类型
📌 本章小结¶
| 概念 | 关键点 |
|---|---|
| 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