条件类型与映射类型
TypeScript条件类型、infer关键字、映射类型、模板字面量类型与类型体操实战。
1. 条件类型基础
1.1 基本语法
条件类型根据类型条件选择不同的类型结果,类似类型层面的三元表达式。
// 基本语法: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true(字面量类型也匹配)
// 实用示例:提取Promise的值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<Promise<number[]>>; // number[]
type P3 = UnwrapPromise<string>; // string(非Promise直接返回)
1.2 条件类型的分发特性
// 当T是联合类型时,条件类型会自动分发
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
// 分发过程:
// ToArray<string> | ToArray<number>
// string[] | number[]
// 阻止分发:用元组包裹
type ToArrayNoDistribute<T> = [T] extends [unknown] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>; // (string | number)[]
1.3 实用条件类型
// NonNullable: 排除null和undefined
type NN = NonNullable<string | null | undefined>; // string
// Extract: 从联合类型中提取匹配的类型
type T1 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type T2 = Extract<string | number | (() => void), Function>; // () => void
// Exclude: 从联合类型中排除匹配的类型
type T3 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type T4 = Exclude<string | number | boolean, number>; // string | boolean
// ReturnType: 获取函数返回类型
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<(x: number) => void>; // void
// Parameters: 获取函数参数类型(元组)
type P1 = Parameters<(a: string, b: number) => void>; // [string, number]
// InstanceType: 获取构造函数实例类型
class MyClass {
x = 10;
y = 20;
}
type Instance = InstanceType<typeof MyClass>; // { x: number; y: number }
2. infer 关键字
2.1 基本用法
infer 在条件类型的 extends 子句中声明类型变量,用于推断类型。
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E1 = ElementOf<string[]>; // string
type E2 = ElementOf<number[]>; // number
// 提取函数第一个参数类型
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F1 = FirstArg<(name: string, age: number) => void>; // string
// 提取Promise链的最终值
type DeepUnwrapPromise<T> = T extends Promise<infer U> ? DeepUnwrapPromise<U> : T;
type Deep = DeepUnwrapPromise<Promise<Promise<Promise<number>>>>; // number
2.2 多个 infer
// 提取函数的参数和返回值
type FunctionInfo<T> = T extends (...args: infer Args) => infer Return
? { args: Args; return: Return }
: never;
type Info = FunctionInfo<(name: string, age: number) => boolean>;
// { args: [string, number]; return: boolean }
// 提取元组的第一个和剩余元素
type HeadTail<T extends any[]> = T extends [infer Head, ...infer Tail]
? { head: Head; tail: Tail }
: never;
type HT = HeadTail<[1, 2, 3, 4]>;
// { head: 1; tail: [2, 3, 4] }
2.3 infer 约束(TypeScript 4.7+)
// 限制infer推断的类型
type FirstIfString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type A = FirstIfString<['hello', 1, 2]>; // 'hello'
type B = FirstIfString<[1, 'hello', 2]>; // never(第一个不是string)
3. 映射类型
3.1 基本映射类型
// 遍历联合类型创建对象类型
type Record<K extends string | number | symbol, V> = {
[P in K]: V;
};
// 基本映射
type StringMap = Record<'name' | 'email', string>;
// { name: string; email: string }
// 遍历键创建新类型
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface User {
id: number;
name: string;
email: string;
}
type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }
3.2 映射类型修饰符
// Partial: 所有属性变为可选
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
// Required: 所有属性变为必选
type MyRequired<T> = {
[P in keyof T]-?: T[P]; // -? 移除可选修饰符
};
// Readonly: 所有属性变为只读
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
// Mutable: 移除只读修饰符
type Mutable<T> = {
-readonly [P in keyof T]: T[P]; // -readonly 移除只读
};
interface Config {
readonly host: string;
readonly port: number;
debug?: boolean;
}
type MutableConfig = Mutable<Config>;
// { host: string; port: number; debug?: boolean }
type RequiredConfig = Required<Config>;
// { readonly host: string; readonly port: number; debug: boolean }
3.3 键重映射(Key Remapping)
// as 子句重映射键
type RemovePrefix<T, P extends string> = {
[K in keyof T as K extends `${P}${infer Rest}` ? Rest : K]: T[K];
};
interface PrefixedEnv {
VITE_API_URL: string;
VITE_APP_TITLE: string;
NODE_ENV: string;
}
type EnvVars = RemovePrefix<PrefixedEnv, 'VITE_'>;
// { API_URL: string; APP_TITLE: string; NODE_ENV: string }
// 过滤属性
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }
4. 模板字面量类型
4.1 基本用法
// 字符串拼接类型
type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// CSS属性类型
type CSSProperty = 'width' | 'height' | 'margin' | 'padding';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';
type CSSRule = `${CSSProperty}-${CSSDirection}`;
// 'margin-top' | 'margin-right' | ... | 'padding-left'
// 内置字符串操作类型
type UppercaseA = Uppercase<'hello'>; // 'HELLO'
type LowercaseA = Lowercase<'HELLO'>; // 'hello'
type CapitalizeA = Capitalize<'hello'>; // 'Hello'
type UncapitalizeA = Uncapitalize<'Hello'>; // 'hello'
4.2 实用模板字面量类型
// 事件监听器类型
type EventMap = {
click: { x: number; y: number };
keydown: { key: string; code: string };
resize: { width: number; height: number };
};
type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;
function on<T extends keyof EventMap>(event: T, handler: EventHandler<T>): void {
// 实现
}
on('click', (e) => {
console.log(e.x, e.y); // 类型安全
});
// 路由参数类型
type ExtractRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type Route1 = ExtractRouteParams<'/users/:id'>; // { id: string }
type Route2 = ExtractRouteParams<'/posts/:postId/comments/:commentId'>;
// { postId: string; commentId: string }
5. 类型体操实战
5.1 DeepPartial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? (T[P] extends Function ? T[P] : DeepPartial<T[P]>) : T[P];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
cache: {
enabled: boolean;
ttl: number;
};
}
type PartialConfig = DeepPartial<Config>;
// 所有嵌套属性都是可选的
const config: PartialConfig = {
database: {
credentials: {
username: 'admin',
},
},
};
5.2 PickByType
// 按值类型选取属性
type PickByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
interface User {
name: string;
age: number;
email: string;
active: boolean;
score: number;
}
type StringProps = PickByType<User, string>; // { name: string; email: string }
type NumberProps = PickByType<User, number>; // { age: number; score: number }
5.3 OmitByType
type OmitByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? never : K]: T[K];
};
type NonStringProps = OmitByType<User, string>;
// { age: number; active: boolean; score: number }
6. 常见问题与解决方案
6.1 条件类型分发导致意外结果
// 问题:分发导致结果不是预期的
type WrapInArray<T> = T extends unknown ? T[] : never;
type R = WrapInArray<string | number>; // string[] | number[]
// 解决方案:阻止分发
type WrapInArrayNoDist<T> = [T] extends [unknown] ? T[] : never;
type R2 = WrapInArrayNoDist<string | number>; // (string | number)[]
6.2 映射类型与索引签名
// 映射类型生成的是索引签名,不是具体属性
type StringRecord = Record<string, string>;
// 等价于 { [key: string]: string }
// 具体键的Record
type SpecificRecord = Record<'a' | 'b', string>;
// { a: string; b: string }
7. 总结与最佳实践
7.1 类型工具选择
| 需求 | 工具 |
|---|---|
| 条件选择类型 | 条件类型 + extends |
| 提取类型 | infer 关键字 |
| 转换对象类型 | 映射类型 |
| 转换键名 | 键重映射 as |
| 字符串类型操作 | 模板字面量类型 |
7.2 最佳实践
- 优先使用内置工具类型:Partial、Required、Pick、Omit等
- 复杂类型添加注释:说明类型变换的意图
- 注意分发特性:联合类型传入条件类型会分发
- infer用于提取:从复杂类型中提取子类型
- 渐进式复杂度:简单类型够用时不要过度抽象