本文献给:
已掌握 TypeScript 泛型、条件类型、索引访问类型等知识的开发者。本文将系统讲解 TypeScript 的高级类型特性,包括映射类型、内置工具类型(Pick、Omit、Record 等)、条件类型、infer 关键字、模板字面量类型、satisfies 运算符、高级类型守卫(is 与 asserts),以及通过类型编程实战实现 DeepReadonly、DeepPartial 和类型安全的 EventBus,帮助你写出更精妙的类型代码。
你将学到:
- 映射类型的语法与内置映射类型(
Partial、Readonly等) - 常用工具类型:
Pick、Omit、Record、Exclude、Extract - 条件类型(
T extends U ? X : Y)与分布式条件类型 infer关键字提取类型内部信息- 模板字面量类型组合字符串类型
satisfies运算符保留具体类型同时满足约束- 高级类型守卫:
is与asserts - 类型编程实战:
DeepReadonly、DeepPartial与类型安全的EventBus
目录
- [一、映射类型(Mapped Types)](#一、映射类型(Mapped Types))
-
- [1.1 基本语法](#1.1 基本语法)
- [1.2 内置映射类型](#1.2 内置映射类型)
- [1.3 映射修饰符](#1.3 映射修饰符)
- [1.4 键名重映射(as 子句,TypeScript 4.1+)](#1.4 键名重映射(as 子句,TypeScript 4.1+))
- 二、实用工具类型详解
-
- [2.1 Pick<T, K>](#2.1 Pick<T, K>)
- [2.2 Omit<T, K>](#2.2 Omit<T, K>)
- [2.3 Record<K, T>](#2.3 Record<K, T>)
- [2.4 Exclude<T, U>](#2.4 Exclude<T, U>)
- [2.5 Extract<T, U>](#2.5 Extract<T, U>)
- [2.6 其他常用工具类型](#2.6 其他常用工具类型)
- [三、条件类型(Conditional Types)](#三、条件类型(Conditional Types))
-
- [3.1 基本用法](#3.1 基本用法)
- [3.2 分布式条件类型](#3.2 分布式条件类型)
- [3.3 内置的 `NonNullable` 实现](#3.3 内置的
NonNullable实现) - [3.4 条件类型与泛型约束的区别](#3.4 条件类型与泛型约束的区别)
- [四、infer 关键字 ------ 从类型中提取部分信息](#四、infer 关键字 —— 从类型中提取部分信息)
-
- [4.1 提取函数返回值类型](#4.1 提取函数返回值类型)
- [4.2 提取函数参数类型](#4.2 提取函数参数类型)
- [4.3 提取数组元素类型](#4.3 提取数组元素类型)
- [4.4 提取 Promise 内部类型](#4.4 提取 Promise 内部类型)
- [4.5 多 infer 位置](#4.5 多 infer 位置)
- [五、模板字面量类型(Template Literal Types)](#五、模板字面量类型(Template Literal Types))
-
- [5.1 基本语法](#5.1 基本语法)
- [5.2 内置字符串操作类型](#5.2 内置字符串操作类型)
- [5.3 与映射类型结合](#5.3 与映射类型结合)
- [5.4 用于事件监听器类型](#5.4 用于事件监听器类型)
- [六、satisfies 运算符 ------ 类型保留的新方式](#六、satisfies 运算符 —— 类型保留的新方式)
-
- [6.1 与类型断言的区别](#6.1 与类型断言的区别)
- [6.2 常见场景:对象字面量](#6.2 常见场景:对象字面量)
- [七、类型守卫高级模式 ------ is 与 asserts 深入](#七、类型守卫高级模式 —— is 与 asserts 深入)
-
- [7.1 自定义类型守卫(is)](#7.1 自定义类型守卫(is))
- [7.2 进阶:泛型守卫与类型谓词](#7.2 进阶:泛型守卫与类型谓词)
- [7.3 断言函数(asserts)](#7.3 断言函数(asserts))
- [7.4 简单断言(asserts condition)](#7.4 简单断言(asserts condition))
- [7.5 断言函数 vs 类型守卫](#7.5 断言函数 vs 类型守卫)
- 八、类型编程实战
-
- [8.1 DeepReadonly](#8.1 DeepReadonly)
- [8.2 DeepPartial](#8.2 DeepPartial)
- [8.3 类型安全的 EventBus](#8.3 类型安全的 EventBus)
- 九、小结
一、映射类型(Mapped Types)
映射类型允许基于旧类型创建新类型,遍历旧类型的属性并应用转换。
1.1 基本语法
typescript
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }
[P in keyof T] 遍历 T 的所有属性名,T[P] 获取属性类型。
1.2 内置映射类型
TypeScript 内置了常用的映射类型:
Partial<T>:所有属性变为可选。Required<T>:所有属性变为必选(移除?)。Readonly<T>:所有属性变为只读。Pick<T, K>:选取部分属性(稍后详述)。Record<K, T>:创建键为 K、值为 T 的对象类型。
typescript
type User = { name: string; age?: number };
type PartialUser = Partial<User>; // { name?: string; age?: number }
type RequiredUser = Required<User>; // { name: string; age: number }
type ReadonlyUser = Readonly<User>; // { readonly name: string; readonly age?: number }
1.3 映射修饰符
+ 和 - 可以添加或移除 readonly 和 ? 修饰符。默认是 +。
typescript
type RemoveReadonly<T> = {
-readonly [P in keyof T]: T[P];
};
type MakeOptional<T> = {
[P in keyof T]?: T[P];
};
type MakeRequired<T> = {
[P in keyof T]-?: T[P]; // 移除可选修饰符
};
1.4 键名重映射(as 子句,TypeScript 4.1+)
可以使用 as 子句重新映射键名。
typescript
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
Capitalize 是内置模板字面量类型(后面会讲)。
二、实用工具类型详解
2.1 Pick<T, K>
从 T 中选取一组属性 K 构成新类型。
typescript
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
// { title: string; completed: boolean; }
2.2 Omit<T, K>
从 T 中排除一组属性 K,与 Pick 相反。
typescript
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type TodoWithoutDescription = Omit<Todo, "description">;
// { title: string; completed: boolean; }
2.3 Record<K, T>
创建一个对象类型,键为 K,值为 T。
typescript
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type UserMap = Record<string, { name: string }>;
const users: UserMap = {
"user1": { name: "Alice" },
"user2": { name: "Bob" }
};
type PageInfo = Record<"home" | "about" | "contact", { title: string }>;
2.4 Exclude<T, U>
从联合类型 T 中排除可赋值给 U 的成员。
typescript
type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
2.5 Extract<T, U>
从联合类型 T 中提取可赋值给 U 的成员。
typescript
type Extract<T, U> = T extends U ? T : never;
type T = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
2.6 其他常用工具类型
NonNullable<T>:排除null和undefined(通过条件类型实现)。ReturnType<T>:获取函数返回值类型(依赖infer)。Parameters<T>:获取函数参数类型元组。ConstructorParameters<T>:获取构造函数参数类型。InstanceType<T>:获取构造函数实例类型。
示例:
typescript
function greet(name: string): string {
return `Hello, ${name}`;
}
type GreetReturn = ReturnType<typeof greet>; // string
type GreetParams = Parameters<typeof greet>; // [name: string]
class User {
constructor(public id: number, public name: string) {}
}
type UserCtorParams = ConstructorParameters<typeof User>; // [id: number, name: string]
type UserInstance = InstanceType<typeof User>; // User
三、条件类型(Conditional Types)
条件类型的语法:T extends U ? X : Y,类似于三元表达式。
3.1 基本用法
typescript
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
3.2 分布式条件类型
当 T 是一个联合类型时,条件类型会分布到每个成员上。
typescript
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
// 如果想避免分布行为,可以用元组包裹
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDistributive<string | number>; // (string | number)[]
3.3 内置的 NonNullable 实现
typescript
type NonNullable<T> = T extends null | undefined ? never : T;
type T = NonNullable<string | null | undefined>; // string
3.4 条件类型与泛型约束的区别
- 泛型约束(
extends)限制可传入的类型范围。 - 条件类型根据传入类型计算出新类型。
typescript
// 约束:只能传有 length 属性的类型
function logLen<T extends { length: number }>(arg: T) {}
// 条件类型:根据类型返回不同结果
type TypeName<T> = T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" : "other";
四、infer 关键字 ------ 从类型中提取部分信息
infer 用于在条件类型中声明一个待推断的类型变量。
4.1 提取函数返回值类型
typescript
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function fn() { return 42; }
type R = ReturnType<typeof fn>; // number
4.2 提取函数参数类型
typescript
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = Parameters<(a: string, b: number) => void>; // [string, number]
4.3 提取数组元素类型
typescript
type ElementType<T> = T extends (infer U)[] ? U : never;
type E = ElementType<string[]>; // string
4.4 提取 Promise 内部类型
typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type R = UnwrapPromise<Promise<Promise<number>>>; // number
4.5 多 infer 位置
可以同时推断多个类型。
typescript
type Both<T> = T extends { a: infer A; b: infer B } ? [A, B] : never;
type R = Both<{ a: string; b: number }>; // [string, number]
五、模板字面量类型(Template Literal Types)
TypeScript 4.1 引入了模板字面量类型,允许在类型级别操作字符串。
5.1 基本语法
typescript
type Greeting = `Hello, ${string}`;
let g: Greeting = "Hello, world"; // OK
// g = "Hi there"; // ❌
type EventName<T extends string> = `${T}Changed`;
type ResizeEvent = EventName<"resize">; // "resizeChanged"
5.2 内置字符串操作类型
Uppercase<StringType>:将字符串转为大写。Lowercase<StringType>:转小写。Capitalize<StringType>:首字母大写。Uncapitalize<StringType>:首字母小写。
typescript
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"typescript">; // "Typescript"
5.3 与映射类型结合
typescript
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
5.4 用于事件监听器类型
typescript
type EventNames = "click" | "focus" | "blur";
type EventHandlers = {
[K in EventNames as `on${Capitalize<K>}`]: (event: Event) => void;
};
// { onClick: (event: Event) => void; onFocus: ...; onBlur: ...; }
六、satisfies 运算符 ------ 类型保留的新方式
satisfies 运算符(TypeScript 4.9+)用于检查一个表达式是否符合某个类型,同时保留表达式的具体类型,而不是拓宽为那个类型。
6.1 与类型断言的区别
typescript
type Colors = "red" | "green" | "blue";
// 断言:丢失了具体值信息,被收窄为 Colors
const color1 = "red" as Colors; // 类型为 "red" 还是 Colors?实际是 Colors
const color2 = "red" satisfies Colors; // 类型为 "red",但仍然满足 Colors
// 实际效果:satisfies 保留字面量类型
const config = {
theme: "dark",
size: 100
} satisfies { theme: "dark" | "light"; size: number };
config.theme; // 类型为 "dark"(字面量),但可以赋值给 "dark"|"light"
// config.theme = "light"; // ❌ 类型 "dark" 不能赋给 "light",但实际不能修改?因为 config 是常量
6.2 常见场景:对象字面量
typescript
type Route = { path: string; children?: Route[] };
const routes = {
home: { path: "/" },
user: { path: "/user", children: [{ path: "/user/profile" }] }
} satisfies Record<string, Route>;
// routes.home.path 类型是 string(字面量"/"被拓宽为string?)
// 实际上 satisfies 不会拓宽,而是推断字面量
更典型的例子:
typescript
type Colors = "red" | "green" | "blue";
const colorSet = {
primary: "red",
secondary: "green"
} satisfies Record<string, Colors>;
colorSet.primary; // 类型为 "red"
// colorSet.primary = "blue"; // ❌ 只读?不是,但常量不可重新赋值
satisfies 解决了既要类型检查,又要保留字面量类型精确信息的需求。
七、类型守卫高级模式 ------ is 与 asserts 深入
7.1 自定义类型守卫(is)
回顾基础:value is Type 作为返回类型。
typescript
function isString(value: unknown): value is string {
return typeof value === "string";
}
7.2 进阶:泛型守卫与类型谓词
typescript
function isArrayOf<T>(value: unknown, check: (item: unknown) => item is T): value is T[] {
return Array.isArray(value) && value.every(check);
}
const isNumber = (x: unknown): x is number => typeof x === "number";
const data: unknown = [1, 2, 3];
if (isArrayOf(data, isNumber)) {
// data 类型为 number[]
console.log(data.reduce((a,b)=>a+b,0));
}
7.3 断言函数(asserts)
断言函数不返回值,如果条件失败则抛出错误,从而在后续代码中收窄类型。
typescript
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Not a string");
}
}
function toUpper(value: unknown) {
assertIsString(value);
return value.toUpperCase(); // value 已收窄为 string
}
7.4 简单断言(asserts condition)
typescript
function assert(condition: any, msg?: string): asserts condition {
if (!condition) throw new Error(msg ?? "Assertion failed");
}
function process(value: string | null) {
assert(value !== null);
// value 为 string
}
7.5 断言函数 vs 类型守卫
- 守卫返回
boolean,用于if分支。 - 断言失败抛出异常,后续代码无条件收窄。
选择:如果希望"一旦检查通过就假定为某类型,否则错误终止",用断言;如果希望分支处理,用守卫。
八、类型编程实战
8.1 DeepReadonly
实现递归的 Readonly,让嵌套对象的所有属性(包括属性中的对象)都变为只读。
typescript
type Primitive = string | number | boolean | symbol | bigint | null | undefined;
type DeepReadonly<T> = T extends Primitive
? T
: T extends Array<infer U>
? ReadonlyArray<DeepReadonly<U>>
: T extends Map<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends Set<infer U>
? ReadonlySet<DeepReadonly<U>>
: { readonly [P in keyof T]: DeepReadonly<T[P]> };
interface User {
name: string;
address: {
city: string;
zip: number;
};
tags: string[];
}
type ReadonlyUser = DeepReadonly<User>;
// 所有属性递归只读
8.2 DeepPartial
递归将属性变为可选,同样处理嵌套对象和数组。
typescript
type DeepPartial<T> = T extends Primitive
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends Map<infer K, infer V>
? Map<DeepPartial<K>, DeepPartial<V>>
: T extends Set<infer U>
? Set<DeepPartial<U>>
: { [P in keyof T]?: DeepPartial<T[P]> };
8.3 类型安全的 EventBus
实现一个事件总线,事件名到回调参数类型的映射。
typescript
type EventMap = {
login: { userId: number };
logout: void;
message: { text: string };
};
class EventBus<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: ((payload: T[K]) => void)[];
} = {};
on<K extends keyof T>(event: K, callback: (payload: T[K]) => void): void {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event]!.push(callback);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
const callbacks = this.listeners[event];
if (callbacks) {
callbacks.forEach(cb => cb(payload));
}
}
off<K extends keyof T>(event: K, callback: (payload: T[K]) => void): void {
const callbacks = this.listeners[event];
if (callbacks) {
this.listeners[event] = callbacks.filter(cb => cb !== callback);
}
}
}
// 使用
const bus = new EventBus<EventMap>();
bus.on("login", (data) => {
console.log(data.userId); // data 类型为 { userId: number }
});
bus.emit("login", { userId: 123 });
bus.emit("logout"); // void 类型,可以不传参数?实际上 payload 类型为 void,可以传 undefined 或不传
注意:void 在事件中可传 undefined 或不传,但为了类型安全,可以调整映射为 void 时 payload 可选。
优化版本:
typescript
type EventMap2 = {
login: { userId: number };
logout: undefined; // 表示不需要数据
message: { text: string };
};
class EventBus2<T extends Record<string, any>> {
// ... 类似实现,emit 时 payload 类型为 T[K] 即可
}
bus2.emit("logout", undefined);
九、小结
| 概念 | 关键语法 / 示例 | 说明 |
|---|---|---|
| 映射类型 | { [P in K]: T[P] } |
遍历属性修改类型 |
Partial / Readonly |
Partial<T>、Readonly<T> |
内置映射工具 |
Pick / Omit |
Pick<T, K>、Omit<T, K> |
选取或排除属性 |
Record |
Record<K, T> |
构造键值对类型 |
Exclude / Extract |
条件类型的分布应用 | 过滤联合类型成员 |
| 条件类型 | T extends U ? X : Y |
类型级别分支 |
infer |
infer R |
提取类型变量 |
| 模板字面量类型 | ${Uppercase<T>} |
字符串类型操作 |
satisfies |
expr satisfies Type |
保留具体类型同时检查兼容性 |
| 断言函数 | asserts value is Type |
失败抛异常,收窄类型 |
| 类型编程实战 | DeepReadonly、DeepPartial、EventBus |
递归映射 + 条件类型 + 泛型 |
觉得文章有帮助?别忘了:
👍 点赞 👍 -- 给我一点鼓励
⭐ 收藏 ⭐ -- 方便以后查看
🔔 关注 🔔 -- 获取更新通知
标签: #TypeScript #高级类型 #工具类型 #条件类型 #infer #模板字面量 #satisfies #类型守卫 #类型编程 #学习笔记 #前端开发