本章学习 TypeScript 类型系统的高级特性,掌握这些就能应对大多数复杂场景。
6.1 联合类型(Union Types)
一个值可以是多种类型之一,用 | 连接:
let id: string | number;
id = "abc"; // ✅
id = 123; // ✅
id = true; // ❌
// 函数参数
function printId(id: string | number): void {
// 只能使用两种类型共有的方法
console.log(id.toString()); // ✅ 两者都有 toString
// 需要缩小类型后才能用特有方法
if (typeof id === "string") {
console.log(id.toUpperCase()); // ✅
} else {
console.log(id.toFixed(2)); // ✅
}
}
可辨识联合(Discriminated Union)⭐
这是 TypeScript 中最常用的模式之一:
// 每个类型有一个共同的"标签"属性
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height; // TS 知道有 width 和 height
case "triangle":
return (shape.base * shape.height) / 2;
}
}
// 实际应用:Redux Action
type Action =
| { type: "INCREMENT"; payload: number }
| { type: "DECREMENT"; payload: number }
| { type: "RESET" };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "INCREMENT":
return state + action.payload;
case "DECREMENT":
return state - action.payload;
case "RESET":
return 0;
}
}
6.2 交叉类型(Intersection Types)
用 & 将多个类型合并为一个类型:
type HasName = { name: string };
type HasAge = { age: number };
type HasEmail = { email: string };
type Person = HasName & HasAge & HasEmail;
// 等同于 { name: string; age: number; email: string }
const person: Person = {
name: "张三",
age: 25,
email: "a@b.com",
};
Mixin 模式
type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };
type User = {
id: number;
name: string;
} & Timestamped & SoftDeletable;
// User 拥有所有属性
6.3 类型守卫(Type Guards)
类型守卫让你在运行时缩小类型范围:
typeof 守卫
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
return " ".repeat(padding) + value; // padding: number
}
return padding + value; // padding: string
}
instanceof 守卫
class Dog {
bark() { console.log("汪!"); }
}
class Cat {
meow() { console.log("喵!"); }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // TS 知道是 Dog
} else {
animal.meow(); // TS 知道是 Cat
}
}
in 守卫
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // Fish
} else {
animal.fly(); // Bird
}
}
自定义类型守卫(is)
interface Cat {
type: "cat";
meow(): void;
}
interface Dog {
type: "dog";
bark(): void;
}
// 返回类型 `animal is Cat` 是类型谓词
function isCat(animal: Cat | Dog): animal is Cat {
return animal.type === "cat";
}
function handle(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow(); // TS 知道是 Cat
} else {
animal.bark(); // TS 知道是 Dog
}
}
断言函数(asserts)
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Not a string!");
}
}
function process(value: unknown) {
assertIsString(value);
// 从这里开始,value 的类型是 string
console.log(value.toUpperCase());
}
6.4 类型缩小(Type Narrowing)
TypeScript 通过控制流分析自动缩小类型:
function example(x: string | number | boolean) {
if (typeof x === "string") {
x; // string
} else if (typeof x === "number") {
x; // number
} else {
x; // boolean
}
}
// 真值检查
function printName(name: string | null | undefined) {
if (name) {
console.log(name.toUpperCase()); // string(排除了 null 和 undefined)
}
}
// 相等检查
function compare(a: string | number, b: string | boolean) {
if (a === b) {
a; // string(唯一两者都可能的类型)
b; // string
}
}
6.5 条件类型(Conditional Types)
类似三元运算符,但用于类型:
// 语法:T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// 实际用途:根据输入推断输出
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number[][]>; // number[]
type C = Flatten<boolean>; // boolean
infer 关键字
infer 用于在条件类型中"推断"出某个类型:
// 提取函数返回类型(ReturnType 的实现原理)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type A = MyReturnType<() => string>; // string
type B = MyReturnType<(x: number) => boolean>; // boolean
// 提取 Promise 包裹的类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<number[]>; // number
分布式条件类型
当联合类型传入条件类型时,会自动"分发"到每个成员:
type ToArray<T> = T extends any ? T[] : never;
// 分发:ToArray<string> | ToArray<number>
type Result = ToArray<string | number>; // string[] | number[]
// 如果不想分发,用方括号包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>; // (string | number)[]
6.6 映射类型(Mapped Types)
基于已有类型创建新类型,遍历每个属性:
// 语法:{ [K in keyof T]: 新类型 }
// 把所有属性变成可选(Partial 的实现原理)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// 把所有属性变成只读(Readonly 的实现原理)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// 把所有属性的值变成 boolean
type Flags<T> = {
[K in keyof T]: boolean;
};
interface User {
name: string;
age: number;
email: string;
}
type UserFlags = Flags<User>;
// { name: boolean; age: boolean; email: boolean }
键的重映射(as)
// 给所有属性名加前缀
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }
// 过滤:只保留 string 类型的属性
type StringProps<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStrings = StringProps<User>;
// { name: string; email: string }
去除修饰符
// - 去除可选
type Required<T> = {
[K in keyof T]-?: T[K];
};
// - 去除只读
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
6.7 模板字面量类型
将字符串字面量类型进行组合:
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
// 自动生成所有组合
type CSSClass = `${Size}-${Color}`;
// "small-red" | "small-blue" | "small-green"
// | "medium-red" | "medium-blue" | "medium-green"
// | "large-red" | "large-blue" | "large-green"
// 事件名生成
type EventName<T extends string> = `on${Capitalize<T>}`;
type MouseEvents = EventName<"click" | "mousedown" | "mouseup">;
// "onClick" | "onMousedown" | "onMouseup"
内置字符串类型工具
type A = Uppercase<"hello">; // "HELLO"
type B = Lowercase<"HELLO">; // "hello"
type C = Capitalize<"hello">; // "Hello"
type D = Uncapitalize<"Hello">; // "hello"
6.8 索引访问类型
通过索引访问其他类型的子类型:
interface User {
name: string;
age: number;
address: {
city: string;
street: string;
};
}
type Name = User["name"]; // string
type Age = User["age"]; // number
type Address = User["address"]; // { city: string; street: string }
type City = User["address"]["city"]; // string
// 联合索引
type NameOrAge = User["name" | "age"]; // string | number
// 用 keyof 获取所有值类型
type UserValues = User[keyof User]; // string | number | { city: string; street: string }
// 数组元素类型
type Arr = string[];
type ArrElement = Arr[number]; // string
6.9 typeof 类型操作符
从值推断类型:
const config = {
host: "localhost",
port: 3000,
debug: true,
};
// 从值中提取类型
type Config = typeof config;
// { host: string; port: number; debug: boolean }
// 常用于获取函数类型
function createUser(name: string, age: number) {
return { name, age, id: Math.random() };
}
type CreateUserFn = typeof createUser;
// (name: string, age: number) => { name: string; age: number; id: number }
type UserType = ReturnType<typeof createUser>;
// { name: string; age: number; id: number }
as const 断言
// 不用 as const
const colors = ["red", "green", "blue"]; // string[]
// 用 as const —— 变成只读的字面量类型
const colors2 = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = (typeof colors2)[number]; // "red" | "green" | "blue"
// 对象也可以
const config = {
endpoint: "/api",
timeout: 3000,
} as const;
// { readonly endpoint: "/api"; readonly timeout: 3000 }
6.10 satisfies 操作符
TS 4.9+ 新增,检查类型的同时保留推断:
type Color = "red" | "green" | "blue";
type Theme = Record<string, Color | Color[]>;
// 用 satisfies:既检查类型,又保留字面量推断
const theme = {
primary: "red",
secondary: "blue",
gradients: ["red", "green"],
} satisfies Theme;
theme.primary; // 类型是 "red"(不是 string | string[])
theme.gradients; // 类型是 ("red" | "green")[]
// 对比:如果用类型注解
const theme2: Theme = {
primary: "red",
secondary: "blue",
gradients: ["red", "green"],
};
theme2.primary; // 类型是 Color | Color[](丢失了具体信息)
📝 练习
- 定义一个可辨识联合
Result<T>,成功时包含data: T,失败时包含error: string - 写一个自定义类型守卫
isNonNull<T>(value: T | null | undefined): value is T - 用条件类型实现
IsArray<T>:如果是数组返回 true,否则返回 false - 用映射类型实现
Nullable<T>:所有属性值可以为 null
// 参考答案
// 1
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function handleResult(result: Result<string>) {
if (result.success) {
console.log(result.data); // TS 知道有 data
} else {
console.log(result.error); // TS 知道有 error
}
}
// 2
function isNonNull<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const values = [1, null, 2, undefined, 3];
const filtered = values.filter(isNonNull); // number[]
// 3
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<number>; // false
// 4
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
name: string;
age: number;
}
type NullableUser = Nullable<User>;
// { name: string | null; age: number | null }