类型体操(Type Manipulation)
核心问题解析
问题一:能不能让类型自动推导嵌套属性?
我们经常需要从深层嵌套对象中提取类型:
ts
const user = {
id: 1,
profile: {
name: "Alice",
address: {
city: "Beijing",
zip: "100000"
}
}
};
// 希望:Get<typeof user, 'profile.address.city'> → string✅ 答案:可以!通过递归类型 + 模板字面量类型实现。
问题二:如何实现类型递归?
递归类型就是“类型引用自身”:
ts
interface TreeNode {
value: number;
children: TreeNode[]; // 引用自身
}在工具类型中,我们也可以写递归:
ts
type DeepReadonly<T> = {
readonly [K in keyof T]:
T[K] extends object ? DeepReadonly<T[K]> : T[K];
};⚠️ 关键:必须有终止条件(如 T[K] extends object ? ... : T[K]),否则会无限展开。
问题三:字符串字面量类型能操作吗?
TypeScript 4.1+ 支持模板字面量类型,可以拼接、解析字符串类型:
ts
type EventName = `on${Capitalize<string>}`;
// 如:'onMouseUp', 'onKeyDown'
type Join<A, B> = `${A}.${B}`;
type T = Join<'user', 'profile'>; // 'user.profile'✅ 答案:是的,字符串字面量类型可以参与类型计算,是“类型编程”的高级武器。
学习目标详解
目标一:实现递归类型(注意终止条件)
递归类型常用于处理嵌套结构。
✅ 实现 DeepPartial<T>:深度可选
ts
type DeepPartial<T> = {
[K in keyof T]?:
T[K] extends object
? T[K] extends Function // 避免函数也被递归
? T[K]
: DeepPartial<T[K]>
: T[K];
};
// 测试
interface User {
id: number;
profile: {
name: string;
contacts: {
email: string;
}
}
}
type PartialUser = DeepPartial<User>;
// profile?: { name?: string; contacts?: { email?: string } }📌 终止条件:
T[K] extends object:只对对象递归T[K] extends Function:排除函数,避免无限递归
目标二:使用模板字面量类型拼接字符串类型
1. 基本拼接
ts
type Path<T, U> = `${T}.${U}`;
type T1 = Path<'user', 'profile'>; // 'user.profile'2. 联合类型分发
ts
type Events = 'click' | 'hover';
type Handler = `${Events}Handler`;
// 'clickHandler' | 'hoverHandler'3. 大小写转换(内置)
ts
type T2 = Capitalize<'hello'>; // 'Hello'
type T3 = Uppercase<'abc'>; // 'ABC'
type T4 = Uncapitalize<'Hello'>; // 'hello'
type T5 = Lowercase<'ABC'>; // 'abc'目标三:掌握类型递归与分布式条件类型
分布式条件类型回顾
当 T 是联合类型时,T extends U ? X : Y 会自动分发:
ts
type Box<T> = T extends any ? [T] : never;
type T = Box<'a' | 'b'>; // [a] | [b]结合递归:实现 Flatten<T>
ts
type Flatten<T> = T extends Array<infer Item>
? Flatten<Item> // 递归展开
: T; // 终止:非数组直接返回
type T6 = Flatten<number[]>; // number
type T7 = Flatten<string[][][]>; // string
type T8 = Flatten<(number | string)[]>; // number | string目标四:实现 DeepPartial、Flatten、Get<T, 'a.b.c'> 等高级类型
1. DeepPartial<T>(已实现)
见上文。
2. Flatten<T>(已实现)
见上文。
3. Get<T, P>:路径访问类型
实现类似 lodash.get 的类型安全版本。
ts
type Get<T, P extends string> =
P extends `${infer K}.${infer R}`
? K extends keyof T
? Get<T[K], R> // 递归访问
: never
: P extends keyof T
? T[P]
: never;
// 测试
interface Config {
api: {
url: string;
timeout: number;
};
ui: {
theme: 'light' | 'dark';
}
}
type ApiUrl = Get<Config, 'api.url'>; // string
type Theme = Get<Config, 'ui.theme'>; // 'light' | 'dark'
type Invalid = Get<Config, 'api.port'>; // never(不存在)📌 关键技术:
infer K和infer R:拆分字符串字面量- 递归调用
Get<T[K], R> keyof T安全检查
总结:本节核心要点
| 技术 | 用途 | 示例 |
|---|---|---|
| 递归类型 | 处理嵌套结构 | DeepPartial, Flatten |
| 模板字面量 | 字符串类型计算 | ${A}.${B}, Capitalize |
| 分布式条件类型 | 联合类型自动分发 | Box<T> = T extends any ? [T] : never |
infer 推断 | 提取类型片段 | infer K, infer R |
📌 类型体操的本质是:用类型表达逻辑,让编译器替我们做类型推导。
练习题
练习题 1:实现 DeepReadonly<T>
要求所有嵌套属性都变为 readonly,但函数保持原样。
ts
type DeepReadonly<T> = {
readonly [K in keyof T]:
T[K] extends Function
? T[K]
: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
// 测试
interface Nested {
a: string;
b: {
c: number;
d: () => void;
};
}
type RO = DeepReadonly<Nested>;
// b: { readonly c: number; d: () => void }练习题 2:实现 TupleToUnion<T>
将元组类型转为联合类型:
ts
type TupleToUnion<T> = T extends Array<infer U> ? U : never;
type T1 = TupleToUnion<[string, number]>; // string | number练习题 3:实现 CamelToSnake<T>
将驼峰命名的字符串字面量转为蛇形命名:
ts
type CamelToSnake<S extends string> =
S extends `${infer First}${infer R}`
? R extends Uncapitalize<R>
? `${Lowercase<First>}${CamelToSnake<R>}`
: `${Lowercase<First>}_${CamelToSnake<R>}`
: S;
// 更准确的实现(基于大写字母拆分):
type CamelToSnake<S extends string> =
S extends `${infer L}${infer M}${infer R}`
? M extends Uppercase<M>
? `${L}_${Lowercase<M>}${CamelToSnake<R>}`
: `${L}${CamelToSnake<`${M}${R}`>}`
: S;
type T2 = CamelToSnake<'userProfile'>; // 'user_profile'
type T3 = CamelToSnake<'APIResponse'>; // 'a_p_i_response'练习题 4:实现 Merge<T, U>
合并两个对象类型,U 的属性覆盖 T:
ts
type Merge<T, U> = Omit<T, keyof U> & U;
type T4 = Merge<{ a: 1; b: 2 }, { b: 3; c: 4 }>;
// { a: 1; b: 3; c: 4 }练习题 5:实现 GetRecursive<T, Path>
增强版 Get,支持数组索引访问:
ts
type GetRecursive<T, P> =
P extends `${infer K}.${infer R}`
? K extends keyof T
? GetRecursive<T[K], R>
: never
: P extends keyof T
? T[P]
: P extends `${number}`
? T extends Array<infer Item> ? Item : never
: never;
// 测试
type Users = { name: string }[];
type T5 = GetRecursive<{ users: Users }, 'users.0.name'>; // string