高级类型操作
用类型编程,让类型系统为你工作
🎯 学习目标
学完本章,你将能够:
- 能够使用 keyof 和索引访问类型动态查询对象属性
- 能够运用映射类型批量转换接口属性
- 能够理解条件类型(extends ? :)与分布式条件类型
- 能够使用 infer 在条件类型中提取和推断子类型
- 能够运用模板字面量类型和 as 键重映射实现字符串类型操作
先看一个问题
如何从一个接口中提取所有属性名?如何让所有属性变成可选?如何自动推导 getter 的命名?
interface User {
name: string;
age: number;
email: string;
}
// 想要:提取属性名 → "name" | "age" | "email"
// 想要:全部变可选 → { name?: string; age?: number; email?: string }
// 想要:自动生成 getter → { getName: () => string; getAge: () => number }
这些需求用"值"的编程思维很难实现。TypeScript 提供了一套类型级别的操作工具:keyof、映射类型、条件类型、模板字面量类型。它们组合起来,就是"类型编程"。
Part 1: keyof
获取类型的所有属性名,生成联合类型
第25章我们用 keyof 做过简单的属性访问。现在来正式认识它——keyof T 能把一个接口的所有属性名提取成联合类型。配合索引访问 T[K],你就能在编译期保证属性名不会拼错。这是整个"类型编程"的起点。
keyof 索引查询
把类型的属性名提取出来
interface User {
name: string;
age: number;
email: string;
}
type UserKeys = keyof User; // "name" | "age" | "email"
// 索引访问类型 T[K] —— 获取某个属性的类型
type UserName = User["name"]; // string
type UserAge = User["age"]; // number
// 获取所有值的类型
type UserValues = User[keyof User]; // string | number
// 实际应用:安全的属性访问函数
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { name: "张三", age: 25, email: "a@b.c" };
const name = getProp(user, "name"); // string
const age = getProp(user, "age"); // number
// getProp(user, "phone"); // 报错!"phone" 不存在
嵌套索引访问
深层属性类型也能提取
// 数组的索引访问
type StringArray = string[];
type Item = StringArray[number]; // string
// 嵌套索引访问
interface Config {
database: {
host: string;
port: number;
};
cache: {
ttl: number;
maxSize: number;
};
}
type DbHost = Config["database"]["host"]; // string
type CacheConfig = Config["cache"];
// { ttl: number; maxSize: number }
就像 JS 中用 obj["a"]["b"] 访问嵌套属性一样,类型中也可以链式索引访问。
Part 2: 映射类型
基于旧类型生成新类型——对每个属性做变换
有了 keyof,你已经能拿到属性名了。映射类型更进一步——它像 for 循环一样遍历每个属性,对值类型做变换。想让所有属性变可选?变只读?变 string?一行映射类型搞定。前面第25章用过的 Partial、Readonly,其实就是映射类型的语法糖。
映射类型基础
遍历所有属性,逐一变换
// 让所有属性可选
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// 让所有属性只读
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// 让所有属性可空
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User { name: string; age: number; }
type PartialUser = MyPartial<User>;
// { name?: string; age?: number }
type ReadonlyUser = MyReadonly<User>;
// { readonly name: string; readonly age: number }
映射类型进阶
变换值类型和从联合生成对象
// 变换值类型:所有属性都变成 string
type Stringify<T> = {
[K in keyof T]: string;
};
interface User { name: string; age: number; }
type StringUser = Stringify<User>;
// { name: string; age: string }
// 从联合类型生成对象
type Flags = "hot" | "new" | "sale";
type FeatureFlags = {
[K in Flags]: boolean;
};
// { hot: boolean; new: boolean; sale: boolean }
映射类型的语法 [K in keyof T] 可以理解为一个 for 循环——遍历 T 的每个属性 K,然后对值类型 T[K] 做变换。
Part 3: 条件类型
类型层面的 if-else,根据条件选择类型
映射类型能遍历属性,但它没法做判断——"如果这个属性是函数,就保留,否则去掉"这种逻辑怎么办?条件类型就是类型层面的 if-else。配合 infer 关键字,还能在匹配的过程中"顺手"提取子类型,像正则的捕获组一样。
条件类型
T extends U ? X : Y —— 类型的 if-else
// 基本条件类型
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<"hello">; // "yes"
type B = IsString<42>; // "no"
type C = IsString<string | number>; // "yes" | "no"(分布式)
// 实际应用:类型守卫
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T1 = TypeName<"hello">; // "string"
type T2 = TypeName<42>; // "number"
type T3 = TypeName<() => void>; // "function"
infer 推断
在条件类型中"捕获"子类型
// 提取函数返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type FnReturn = MyReturnType<() => string>; // string
type FnReturn2 = MyReturnType<(x: number) => boolean>; // boolean
// 提取函数参数类型
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type FnParams = MyParameters<(x: string, y: number) => void>;
// [x: string, y: number]
// 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<number>; // number
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Items = ElementOf<string[]>; // string
infer 就像正则表达式的"捕获组"——在匹配过程中把某个部分提取出来。
分布式条件类型
联合类型会自动"展开"
// 当 T 是联合类型时,条件类型会自动分发
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// 等价于 ToArray<string> | ToArray<number>
// 结果: string[] | number[]
// 阻止分发:用 [T] 包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>;
// 结果: (string | number)[]
string[] | number[] 是"字符串数组或数字数组",(string | number)[] 是"元素可以是字符串或数字的数组"。分布式行为会影响结果类型。

图:分布式条件类型的展开过程
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 上方路径(分布式):ToArray<string|number> → 拆分 → string[]|number[] - 下方路径(非分布式):ToArray<string|number> → 整体 → (string|number)[] - 标注:"裸类型参数→自动分发 | 包在[]中→禁止分发" - 配色:分布式绿色系,非分布式橙色系,16:9
never 类型过滤
分布式条件类型 + never = 强力过滤器
// 从联合类型中过滤掉不需要的成员
type OnlyStrings<T> = T extends string ? T : never;
type Result = OnlyStrings<"hello" | 42 | "world" | true>;
// "hello" | "world" —— 数字和布尔值被 never 过滤掉了
// 手写 Extract 和 Exclude
type MyExtract<T, U> = T extends U ? T : never;
type MyExclude<T, U> = T extends U ? never : T;
type T1 = MyExtract<"a" | "b" | 1 | 2, string>; // "a" | "b"
type T2 = MyExclude<"a" | "b" | 1 | 2, string>; // 1 | 2
原理:分布式条件类型把联合类型逐个检查,不满足条件的变成 never。而 never 在联合类型中会被自动消除,就像 0 在加法中被忽略一样——不过这个类比只适用于联合类型,在元组和对象中 never 不会自动消失。
提取方法名
用 never 过滤出对象中所有的方法
// 提取 T 中所有方法名
type GetMethods<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T];
interface UserService {
name: string;
version: number;
getUser(): { id: string };
updateUser(id: string): void;
deleteUser(id: string): boolean;
}
type Methods = GetMethods<UserService>;
// "getUser" | "updateUser" | "deleteUser"
// 同理:提取所有非函数属性名
type GetDataProps<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K
}[keyof T];
type DataProps = GetDataProps<UserService>;
// "name" | "version"
[keyof T] 后缀。映射类型产生的是对象类型 { name: never; getUser: "getUser"; ... },需要用索引访问 [keyof T] 把值提取出来,never 才会被自动过滤。
Part 4: 模板字面量
在类型层面拼接字符串,自动生成命名模式
你写 React 时有没有想过,onClick、onFocus 这些事件名能不能自动推导?模板字面量类型就能做到。它可以在类型层面拼接字符串,配合 Capitalize 等内置工具,自动生成 getName、onClick 这样的命名模式。
模板字面量类型
类型层面的字符串拼接
// 自动生成事件处理器命名
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// 内置字符串操作类型
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"
// CSS 属性类型
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSRule = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"
// 动态事件类型
type DomainEvent<T extends string> = `${T}Changed` | `${T}Deleted`;
type UserEvent = DomainEvent<"user">; // "userChanged" | "userDeleted"
Part 5: 键重映射
用 as 重命名或过滤映射类型的键
映射类型能变换属性值,但属性名本身呢?键重映射用 as 关键字实现了这个能力——你可以给属性加前缀、改命名,甚至把不需要的属性过滤掉。前几节学的映射类型加上键重映射,就是完整的"类型编程工具箱"了。
as 键重映射
重命名键或过滤掉不需要的属性
// 重命名键:给所有属性加前缀
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
interface Config { host: string; port: number; }
type DbConfig = Prefixed<Config, "db">;
// { dbHost: string; dbPort: number }
// 过滤键:只保留函数类型的属性
type FunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
interface Mix {
name: string;
greet(): void;
age: number;
calc(x: number): number;
}
type OnlyFns = FunctionProperties<Mix>;
// { greet: () => void; calc: (x: number) => number }
综合实战
自动生成 Getter 类型
// 映射类型 + 模板字面量 + 键重映射
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
// 事件处理器映射
type EventHandlers<T extends Record<string, any>> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (data: T[K]) => void;
};
type Events = {
click: { x: number; y: number };
keypress: { key: string };
};
type Handlers = EventHandlers<Events>;
// { onClick: (data: { x: number; y: number }) => void;
// onKeypress: (data: { key: string }) => void }
高级类型操作不必一次全部掌握。从实际需求出发,遇到问题再查对应的工具类型即可。
字符串解析模式
用模板字面量 + infer 解析字符串结构
// 从路由字符串中提取参数名
type ParseRoute<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? Param | ParseRoute<Rest>
: S extends `${string}:${infer Param}`
? Param
: never;
type Params = ParseRoute<"/users/:userId/posts/:postId">;
// "userId" | "postId"
// 类型安全的路由定义
type RouteParams<T extends string> = ParseRoute<T>;
function createRoute<T extends string>(path: T) {
return {
path,
build: (params: Record<RouteParams<T>, string>) => {
/* ... */
}
};
}
const userPostRoute = createRoute("/users/:userId/posts/:postId");
// build 的参数类型: Record<"userId" | "postId", string>
模板字面量类型 + infer 就像类型层面的正则表达式。你可以在 extends 的右侧写模式串,用 infer 捕获"捕获组"。不过它不支持量词和复杂语法,只适合结构简单的字符串。
更多解析模式
CSS 属性名变换、事件名提取
// CSS 属性名转换:kebab-case → camelCase
type KebabToCamel<S extends string> =
S extends `${infer Head}-${infer Tail}`
? `${Head}${Capitalize<KebabToCamel<Tail>>}`
: S;
type CSS = KebabToCamel<"background-color">; // "backgroundColor"
type CSS2 = KebabToCamel<"border-top-left-radius">; // "borderTopLeftRadius"
// 提取事件处理器中的事件名
type ExtractEventName<T extends string> =
T extends `on${infer Event}` ? Uncapitalize<Event> : never;
type Click = ExtractEventName<"onClick">; // "click"
type Focus = ExtractEventName<"onFocus">; // "focus"
type NotEvent = ExtractEventName<"onClickHandler">; // "clickHandler"
infer 是"贪婪匹配"还是"非贪婪匹配"取决于位置。在上面的 on${infer Event} 中,Event 会匹配到字符串末尾(贪婪)。如果需要精确匹配,要在模式中写清楚边界。
五大操作对比
一张表总结高级类型操作
| 操作 | 语法 | 作用 |
|---|---|---|
| keyof | keyof T | 提取所有属性名为联合类型 |
| 索引访问 | T[K] | 获取某个属性的类型 |
| 映射类型 | [K in keyof T] | 遍历属性生成新类型 |
| 条件类型 | T extends U ? X : Y | 根据条件选择类型 |
| 模板字面量 | `${A}${B}` | 拼接字符串类型 |
递归类型
DeepPartial、DeepReadonly——类型也能递归
内置的 Partial<T> 只做一层。嵌套对象呢?需要自己写递归类型。
// 内置 Partial 只处理第一层
interface Config {
db: { host: string; port: number };
cache: { ttl: number };
}
type P = Partial<Config>;
// { db?: { host: string; port: number }; cache?: { ttl: number } }
// db 里面的属性并没有变可选!
// DeepPartial:递归地把所有属性变可选
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
type DP = DeepPartial<Config>;
// { db?: { host?: string; port?: number }; cache?: { ttl?: number } }
// DeepReadonly:递归地把所有属性变只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
递归类型的套路是固定的:用条件类型判断属性值是否是对象,如果是就递归处理,否则直接返回。
映射变换全景
原接口 → 遍历属性 → 变换 → 新接口
映射类型本质上就是一个"类型的 for 循环"。让我们用 Partial 举例,看看变换的每一步:
interface User { name: string; age: number; email: string; }
// MyPartial 的变换过程:
// 第1步:keyof User → "name" | "age" | "email"
// 第2步:遍历每个属性 K
// K = "name" → name?: string
// K = "age" → age?: number
// K = "email" → email?: string
// 第3步:合并成新类型
type Result = { name?: string; age?: number; email?: string; }

图:映射类型的变换流程
【AI 生图提示词】请生成一张中文技术流程图,风格简洁、配色柔和、适合教学: - 画面主体:从左到右的变换管道 - 左侧:原始接口 User { name: string, age: number, email: string }(蓝色实线边框卡片) - 中间变换管道(三个步骤): - keyof 提取属性名 → "name" | "age" | "email" - 遍历每个属性名 - 应用 ? 修饰符(属性变为可选) - 右侧:结果类型 { name?: string, age?: number, email?: string }(绿色虚线边框卡片) - 底部标注:"映射类型是纯函数——输入类型,输出新类型,原类型不变" - 配色建议:浅灰背景,输入蓝色系,变换橙色箭头,输出绿色系 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
映射类型的强大之处在于它是一个纯函数——输入一个类型,输出一个新类型,原类型不变。
协变与逆变
函数参数类型的兼容性规则
这个问题在写泛型回调时经常碰到。先看一个"反直觉"的例子:
// 疑问:下面的赋值安全吗?
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;
let handler: AnimalHandler = (a) => console.log(a.name);
// 把 AnimalHandler 赋给 DogHandler?
let dogHandler: DogHandler = handler; // 严格模式下报错!
// 协变(Covariance):方向一致
// Dog 是 Animal 的子类型 → Dog[] 也是 Animal[] 的子类型
// 返回值类型是协变的
// 逆变(Contravariance):方向相反
// 函数参数类型是逆变的:
// (animal: Animal) => void 是 (dog: Dog) => void 的子类型
// 因为能处理所有 Animal 的函数,一定能处理 Dog
协变(同方向)
子类型关系保持一致。Dog extends Animal,所以 Promise<Dog> 可以赋给 Promise<Animal>。
逆变(反方向)
子类型关系反转。(x: Animal) => void 可以赋给 (x: Dog) => void。参数越宽泛,函数越通用。
日常开发中不用深究,但遇到泛型回调的类型报错时,想想协变和逆变就能找到原因。

图:协变与逆变的方向性
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 上下两行:上行协变 Array<Dog>→Array<Animal>(同向箭头)标注"生产者" - 下行逆变 Handler<Animal>→Handler<Dog>(反向箭头)标注"消费者" - 右侧口诀:"子类型关系在容器中保持同向,在函数参数中反转" - 配色:协变绿色箭头,逆变橙色箭头,类型层级蓝色,16:9
协变实战指南
生产者场景:子类型关系保持一致
class Animal { name = "动物"; }
class Dog extends Animal { bark() { return "汪"; } }
class Cat extends Animal { meow() { return "喵"; } }
// 协变:Dog[] 可以赋给 Animal[]
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // OK — 数组是协变的
// 因为数组是"生产者"——你从中取出元素,元素至少是 Animal
// 反过来就不行
const mixedAnimals: Animal[] = [new Dog(), new Cat()];
// const dogList: Dog[] = mixedAnimals; // 报错!可能有 Cat
// Promise 也是协变的
async function getDog(): Promise<Dog> { return new Dog(); }
async function useAnimal(): Promise<Animal> {
return getDog(); // OK — Promise<Dog> 赋给 Promise<Animal>
}
// 返回值也是协变:返回 Dog 的函数可以代替返回 Animal 的函数
type AnimalFactory = () => Animal;
type DogFactory = () => Dog;
const makeDog: DogFactory = () => new Dog();
const makeAnimal: AnimalFactory = makeDog; // OK
类比记忆:"生产者协变"——如果某类型是"往外产出"数据的(数组元素、Promise 值、函数返回值),子类型关系保持一致方向。但这个类比不完美——TypeScript 中数组的 push 方法其实是"消费者",所以严格来说数组并不纯粹是生产者。
逆变实战指南
消费者场景:子类型关系反转
type Handler<T> = (event: T) => void;
// 能处理 Animal 的函数,一定能处理 Dog
const animalHandler: Handler<Animal> = (a) => {
console.log(a.name); // 只用了 Animal 的属性
};
const dogHandler: Handler<Dog> = animalHandler; // OK (strictFunctionTypes)
// 把 Dog 传给 animalHandler,只访问 name —— 安全!
// 反过来就危险了:
// const badHandler: Handler<Animal> = (d: Dog) => d.bark();
// badHandler(new Cat()); // 运行时崩溃!Cat 没有 bark()
tsconfig.json 中开启 "strictFunctionTypes": true(包含在 strict 模式中),函数参数才会正确地做逆变检查。关闭此选项时,函数参数会做双向协变——方便但不安全。
事件系统实战
协变 + 逆变在真实项目中的应用
// 类型安全的事件系统
interface EventMap {
click: { x: number; y: number };
keypress: { key: string; ctrl: boolean };
resize: { width: number; height: number };
}
class TypeSafeEventEmitter<T extends Record<string, any>> {
// Handler<T[K]> 是逆变的 —— 需要"宽泛"的处理器
private handlers: { [K in keyof T]?: Array<(data: T[K]) => void> } = {};
on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
(this.handlers[event] ??= []).push(handler);
}
emit<K extends keyof T>(event: K, data: T[K]) {
this.handlers[event]?.forEach(h => h(data));
}
}
const emitter = new TypeSafeEventEmitter<EventMap>();
emitter.on("click", (data) => {
console.log(data.x, data.y); // data 自动推导为 { x: number; y: number }
});
// emitter.on("click", (data) => data.key); // 报错!click 没有 key
// emitter.emit("keypress", { x: 1, y: 2 }); // 报错!数据类型不匹配
协变逆变的核心原则:"生产者协变,消费者逆变"。记住这句话,遇到泛型回调的类型报错就不慌了。
递归 Omit
练习
任务:实现一个 DeepOmit<T, K> 类型,递归地从嵌套对象中排除所有名为 K 的属性。
interface Config {
id: string;
db: { id: string; host: string; port: number };
cache: { id: string; ttl: number };
}
type Clean = DeepOmit<Config, "id">;
// 期望结果:
// { db: { host: string; port: number }; cache: { ttl: number } }
// 所有层级的 id 都被移除
// 你的代码:
参考答案
type DeepOmit<T, K extends string> = {
[P in keyof T as P extends K ? never : P]:
T[P] extends object
? DeepOmit<T[P], K>
: T[P];
};
// 验证
interface Config {
id: string;
db: { id: string; host: string; port: number };
cache: { id: string; ttl: number };
}
type Clean = DeepOmit<Config, "id">;
// { db: { host: string; port: number }; cache: { ttl: number } }
常见问题
问题一:映射类型中 keyof 报错
// 映射类型只能用于类型,不能用于值
type Bad = { [K in keyof obj] }; // 报错!obj 是值不是类型
type Good = { [K in keyof User] }; // OK,User 是类型
问题二:infer 只能在条件类型中使用
// 报错:infer 不能出现在条件类型之外
type Bad<T> = infer R; // 报错!
// 正确:infer 在 extends 右侧
type Good<T> = T extends Promise<infer R> ? R : T;
问题三:Capitalize 对非 string 类型报错
// 需要用 string & K 做类型收窄
type Safe<K> = Capitalize<string & K>; // OK
type Unsafe<K> = Capitalize<K>; // 可能报错
in 操作符收窄
运行时判断 + 类型层面的属性检测
// 运行时:in 操作符收窄联合类型
function processContact(contact: { email: string } | { phone: string }) {
if ("email" in contact) {
// TypeScript 自动收窄为 { email: string }
console.log(contact.email); // OK
// console.log(contact.phone); // 报错
} else {
// 自动收窄为 { phone: string }
console.log(contact.phone); // OK
}
}
// 类型层面:检测是否拥有某属性
type HasEmail<T> = T extends { email: string } ? true : false;
type A = HasEmail<{ name: string; email: string }>; // true
type B = HasEmail<{ name: string; phone: string }>; // false
// 检测可选属性(配合 & {} 技巧)
type IsOptional<T, K extends keyof T> =
{} extends Pick<T, K> ? true : false;
interface User { name: string; age?: number; email: string; }
type OptAge = IsOptional<User, "age">; // true
type OptName = IsOptional<User, "name">; // false
in 和 keyof 的区别:keyof T 在类型层面提取所有属性名(编译时),"x" in obj 在运行时检查对象是否有某个属性。两者常配合使用:keyof 设计类型,in 做运行时守卫。
类型体操实战
组合 keyof + 条件 + 映射,手撕工具类型
// 挑战 1:DeepReadonly —— 递归只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function // 函数不递归
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
interface Config {
db: { host: string; pool: { max: number } };
log: () => void;
}
type Frozen = DeepReadonly<Config>;
// { readonly db: { readonly host: string; readonly pool: { readonly max: number } };
// readonly log: () => void; }
// 挑战 2:RequiredKeys —— 提取所有必填属性的 key
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T];
interface User { id: string; name?: string; age: number; bio?: string; }
type RKeys = RequiredKeys<User>; // "id" | "age"
// 原理拆解:
// 1. [K in keyof T]-? 遍历所有属性并去掉 ?(不影响判断)
// 2. {} extends Pick<T, K> 检查空对象能否赋给只含 K 的类型
// ——如果 K 是可选的,{} 就能赋过去,返回 never
// ——如果 K 是必填的,赋不过去,返回 K
// 3. [keyof T] 把值取出来,never 被过滤
类型体操的解题套路:(1) 用 keyof 拿属性名;(2) 用映射类型遍历;(3) 用条件类型判断;(4) 用 never 过滤。掌握了这四步,大部分工具类型都能手写。
试一试
练习一
任务:手写一个 MyOmit<T, K> 类型,从 T 中排除 K 指定的属性。
// 提示:结合映射类型和 as 过滤
type MyOmit<T, K extends keyof T> = ???
interface User { id: string; name: string; password: string; }
type SafeUser = MyOmit<User, "password">;
// 期望:{ id: string; name: string }
参考答案
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
练习二
任务:写一个 UnwrapPromise<T> 类型,递归解包嵌套的 Promise。
// UnwrapPromise<Promise<Promise<number>>>
// 应该得到 number
// 你的代码:
参考答案
type DeepUnwrap<T> =
T extends Promise<infer U> ? DeepUnwrap<U> : T;
type Test = DeepUnwrap<Promise<Promise<Promise<number>>>>;
// number
📝 要点总结
本章核心知识点
keyof 与索引
keyof T 获取键联合,T[K] 索引访问,嵌套索引
映射类型
{ [K in keyof T]: ... } 批量转换属性
条件类型
T extends U ? X : Y,分布式条件类型自动分发
infer 推断
在条件类型中提取子类型,实现 ReturnType 等工具
模板字面量
类型级字符串操作,as 键重映射,递归类型
下一章
你已经掌握了类型编程的基础工具。下一章我们学习内置工具类型——TypeScript 自带的类型变换利器。