TypeScript 进阶
大约 12 分钟约 3653 字
TypeScript 进阶
简介
TypeScript 进阶涵盖条件类型、映射类型、模板字面量类型和类型体操。掌握这些高级类型工具,能够在框架开发、库封装和复杂业务场景中实现精确的类型安全。TypeScript 的类型系统是图灵完备的,理论上可以用类型系统完成任何计算。但在实际项目中,类型编程应当服务于业务需求,而非炫技。本篇从条件类型入手,逐步深入到 infer 推断、映射类型重映射、模板字面量类型和递归类型,并结合 React 和 Vue 框架的实际场景展示高级类型的应用。
特点
条件类型与 infer
基础条件类型
// 条件类型 — 类似三元表达式,但作用于类型
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'
type C = IsString<string | number>; // 'yes' | 'no'(分布式)
// 分布式条件类型 — 当 T 是联合类型时,条件类型会自动分发
type NonNullable<T> = T extends null | undefined ? never : T;
type D = NonNullable<string | null | undefined>; // string
// 禁止分布式 — 用方括号包裹
type ToArray<T> = [T] extends [any] ? T[] : T[];
type E = ToArray<string | number>; // (string | number)[] — 不分发
// infer 推断 — 在条件类型中"捕获"类型的一部分
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function getUser(id: string): { name: string; age: number } {
return { name: '', age: 0 };
}
type UserReturn = ReturnType<typeof getUser>; // { name: string; age: number }
type UserParams = Parameters<typeof getUser>; // [string]
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;
// 提取 Promise 内部类型(递归)
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Result = Awaited<Promise<Promise<string>>>; // string
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Item = ElementOf<string[]>; // string
type Item2 = ElementOf<number[]>; // number
// 提取函数第一个参数的类型
type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
type First = FirstArg<(name: string, age: number) => void>; // string
// 提取对象中的函数类型
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
interface ApiService {
getUsers(): Promise<User[]>;
createUser(data: User): Promise<User>;
config: { baseUrl: string };
}
type ApiMethods = FunctionKeys<ApiService>; // 'getUsers' | 'createUser'实用类型推断工具
// 从事件处理器中提取事件类型
type ExtractEvent<T> = T extends (...args: [infer E, ...any]) => any ? E : never;
type ClickHandler = ExtractEvent<(e: React.MouseEvent) => void>; // React.MouseEvent
// 从链式调用中提取中间类型
type ChainedResult<T> = T extends { then: (fn: (r: infer R) => any) => any } ? R : never;
type Parsed = ChainedResult<Promise<string>>; // string
// 提取对象中特定类型的值
type ValueOf<T> = T[keyof T];
type Colors = ValueOf<{ primary: string; secondary: string }>; // string
// 提取 getter 返回类型
type GetterType<T, K extends keyof T> = T[K] extends () => infer R ? R : T[K];
class Store {
get count() { return 0; }
name = 'store';
}
type Count = GetterType<Store, 'count'>; // number
type Name = GetterType<Store, 'name'>; // string映射类型与工具类型
自定义映射类型
// 自定义映射类型 — 可选化指定字段
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type Required<T, K extends keyof T> = Omit<T, K> & Required_<Pick<T, K>>;
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
type UserUpdate = Optional<User, 'name' | 'email'>;
// { id: number; name?: string; email?: string; avatar?: string }
// 深层 Partial — 递归将所有属性变为可选
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
cache: {
enabled: boolean;
ttl: number;
};
}
type PartialConfig = DeepPartial<Config>;
// 所有嵌套属性都是可选的
// 深层 Readonly — 递归将所有属性变为只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// 提取特定类型的属性(使用 as 子句重映射键)
type PickByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
type StringProps = PickByType<User, string>; // { name: string; email: string }
type NumberProps = PickByType<User, number>; // { id: number }
// 排除特定类型的属性
type ExcludeByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? never : K]: T[K];
};
type NonStringProps = ExcludeByType<User, string>; // { id: number; avatar?: string }
// 将所有键变为大写
type UppercaseKeys<T> = {
[K in keyof T as K extends string ? Uppercase<K> : K]: T[K];
};
// 将所有键变为只读 + 大写
type StrictConfig = {
readonly [K in keyof Config as Uppercase<K>]: Config[K];
};
// { readonly DATABASE: { ... }; readonly CACHE: { ... } }框架级工具类型
// Vue Props 类型提取
type PropsToRefs<T> = {
[K in keyof T]: Ref<T[K]>;
};
// React 组件 Props 提取
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
type ButtonProps = ComponentProps<typeof Button>;
// 事件处理器类型映射
type EventHandler<T extends Record<string, any>> = {
[K in keyof T as `on${Capitalize<string & K>}`]?: T[K] extends (...args: any[]) => any
? T[K]
: (event: T[K]) => void;
};
type UserEvents = {
login: { userId: string; timestamp: number };
logout: { userId: string };
};
type UserHandlers = EventHandler<UserEvents>;
// { onLogin?: (event: { userId: string; timestamp: number }) => void;
// onLogout?: (event: { userId: string }) => void; }
// 使特定字段不可变
type Immutable<T, K extends keyof T> = Omit<T, K> & { readonly [P in K]: T[P] };
// 构建查询参数类型
type QueryParams<T> = {
[K in keyof T as T[K] extends string | number ? K : never]?: T[K];
};
interface UserFilters {
name: string;
age: number;
active: boolean;
sort: string;
}
type UserQueryParams = QueryParams<UserFilters>;
// { name?: string; age?: number; sort?: string }模板字面量类型
字符串模式类型
// 事件名称生成
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type FocusEvent = EventName<'focus'>; // 'onFocus'
type ChangeEvent = EventName<'change'>; // 'onChange'
// CSS 属性类型安全
type CSSUnit = 'px' | 'rem' | 'em' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;
const width: CSSValue = '100px'; // OK
const height: CSSValue = '50vh'; // OK
// const bad: CSSValue = '100'; // Error — 缺少单位
// 媒体查询类型
type MediaQuery = `@media (${'min' | 'max'}-${'width' | 'height'}: ${CSSValue})`;
const query: MediaQuery = '@media (max-width: 768px)'; // OK
// 路由类型安全
type Routes = '/users' | '/users/:id' | '/posts' | '/posts/:id/tags/:tagId';
type RouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof RouteParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type UserRouteParams = RouteParams<'/users/:id'>; // { id: string }
type TagRouteParams = RouteParams<'/posts/:id/tags/:tagId'>; // { id: string; tagId: string }
// Getter/Setter 方法名
type Getter<T extends string> = `get${Capitalize<T>}`;
type Setter<T extends string> = `set${Capitalize<T>}`;
type UserAccessors = Getter<'name'> | Setter<'name'>; // 'getName' | 'setName'
// 组合键名
type BreadcrumbItem = {
path: string;
label: string;
};
type BreadcrumbKey = `breadcrumb.${number}.${'path' | 'label'}`;
// API 方法名生成
type ApiMethod<T extends string> = `api/${T}`;
type ApiAction = 'getUsers' | 'createUser' | 'deleteUser';
type ApiMethods = ApiMethod<ApiAction>;
// 'api/getUsers' | 'api/createUser' | 'api/deleteUser'递归模板字面量类型
// 拆分路径为元组
type Split<S extends string, D extends string = '.'> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type PathTuple = Split<'a.b.c.d'>; // ['a', 'b', 'c', 'd']
// 连接路径
type Join<T extends string[], D extends string = '/'> =
T extends [] ? '' :
T extends [string] ? T[0] :
T extends [infer First extends string, ...infer Rest extends string[]]
? `${First}${D}${Join<Rest, D>}`
: never;
type Path = Join<['api', 'users', ':id'], '/'>; // 'api/users/:id'
// 替换字符串中的内容
type Replace<S extends string, From extends string, To extends string> =
S extends `${infer Head}${From}${infer Tail}`
? `${Head}${To}${Replace<Tail, From, To>}`
: S;
type Cleaned = Replace<'hello-world-foo', '-', '_'>; // 'hello_world_foo'递归类型与高级模式
递归条件类型
// 深层 Readonly
type DeepReadonly<T> = T extends Function ? T :
T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
// 深层 Partial
type DeepPartial<T> = T extends Function ? T :
T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
// 展平联合类型
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Nested = Flatten<number[][][][]>; // number
// 深层 Omit — 从嵌套对象中移除指定键
type DeepOmit<T, K extends string> = T extends object
? { [P in keyof T as P extends K ? never : P]: DeepOmit<T[P], K> }
: T;
interface NestedData {
id: string;
meta: {
createdAt: string;
updatedAt: string;
secret: string;
};
items: Array<{
name: string;
secret: string;
}>;
}
type Sanitized = DeepOmit<NestedData, 'secret'>;
// { id: string; meta: { createdAt: string; updatedAt: string }; items: Array<{ name: string }> }
// 类型守卫推导
type IsEqual<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Test1 = IsEqual<string, string>; // true
type Test2 = IsEqual<string, number>; // false
type Test3 = IsEqual<any, string>; // false(any 是特殊的)Branded Types(品牌类型)
// Branded Type — 为相同基础类型创建不同语义
type Brand<T, B extends string> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type Email = Brand<string, 'Email'>;
// 创建函数
function createUserId(id: string): UserId {
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes('@')) throw new Error('无效的邮箱');
return email as Email;
}
// 使用 — 类型系统防止混用
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId = createUserId('123');
const orderId = '456' as OrderId;
getUser(userId); // OK
getUser(orderId); // Error — OrderId 不能赋值给 UserId
// 实际应用 — 确保类型安全
interface User {
id: UserId;
email: Email;
}
function updateUser(id: UserId, data: Partial<User>) {
return fetch(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}常见内置工具类型实现原理
// Partial — 所有属性变可选
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Required — 所有属性变必选
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // -? 移除可选标记
};
// Pick — 选取部分属性
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omit — 排除部分属性
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Record — 构造键值对类型
type MyRecord<K extends string | number | symbol, V> = {
[P in K]: V;
};
// Exclude — 从联合类型中排除
type MyExclude<T, U> = T extends U ? never : T;
// Extract — 从联合类型中提取
type MyExtract<T, U> = T extends U ? T : never;
// NonNullable — 排除 null 和 undefined
type MyNonNullable<T> = T extends null | undefined ? never : T;
// Readonly — 所有属性变只读
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};在框架中的实际应用
React 类型工具
// 强制类型断言
function assertNever(x: never): never {
throw new Error(`Unexpected: ${x}`);
}
type ActionType = 'fetch' | 'create' | 'update' | 'delete';
function handleAction(type: ActionType) {
switch (type) {
case 'fetch': return 'fetching';
case 'create': return 'creating';
case 'update': return 'updating';
case 'delete': return 'deleting';
default: return assertNever(type); // 编译时检查是否覆盖所有情况
}
}
// 精确的 useState 类型
function useTypedState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] {
// ...
}
// Props 差异类型
type Diff<T, U> = T extends U ? never : T;
type RemovedProps<T, U> = { [K in keyof T as K extends keyof U ? never : K]: T[K] };
// 组件 Props 继承与覆盖
interface BaseInputProps {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
error?: string;
}
type TextInputProps = BaseInputProps & {
type: 'text' | 'email' | 'password';
placeholder?: string;
maxLength?: number;
};
type NumberInputProps = BaseInputProps & {
type: 'number';
min?: number;
max?: number;
step?: number;
};
type InputProps = TextInputProps | NumberInputProps;
function Input(props: InputProps) {
if (props.type === 'number') {
// TypeScript 知道这里是 NumberInputProps
return <input type="number" min={props.min} max={props.max} />;
}
// TypeScript 知道这里是 TextInputProps
return <input type={props.type} placeholder={props.placeholder} />;
}Vue 类型工具
// defineProps 类型推断
interface ButtonProps {
variant: 'primary' | 'danger' | 'ghost';
size: 'small' | 'medium' | 'large';
disabled?: boolean;
}
// emit 类型定义
const emit = defineEmits<{
(e: 'change', value: string): void;
(e: 'update:modelValue', value: string): void;
(e: 'submit', data: FormData): void;
}>();
// provide/inject 类型安全
interface ThemeConfig {
primaryColor: string;
borderRadius: number;
fontSize: number;
}
const themeKey: InjectionKey<ThemeConfig> = Symbol('theme');
// provide
provide(themeKey, reactive({ primaryColor: '#1890ff', borderRadius: 4, fontSize: 14 }));
// inject — 自动推断类型
const theme = inject(themeKey)!; // ThemeConfig
// Composable 返回类型
function useFetch<T>(url: string): {
data: Ref<T | null>;
loading: Ref<boolean>;
error: Ref<Error | null>;
refresh: () => Promise<void>;
} {
const data = ref<T | null>(null);
const loading = ref(false);
const error = ref<Error | null>(null);
async function refresh() {
loading.value = true;
try {
const res = await fetch(url);
data.value = await res.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
refresh();
return { data: readonly(data), loading: readonly(loading), error: readonly(error), refresh };
}优点
缺点
总结
TypeScript 进阶类型工具包括条件类型、映射类型和模板字面量类型。infer 关键字在条件类型中推断和提取子类型。映射类型的 as 子句支持键重映射,结合条件类型实现属性过滤。模板字面量类型用于生成字符串模式的类型约束,如路由参数和 CSS 值。递归条件类型处理深层嵌套对象。Branded Types 区分相同基础类型的不同语义。建议在库和框架开发中使用高级类型,业务代码保持简洁实用。
关键知识点
- 条件类型配合 infer 实现类型推断和提取。
- 映射类型可以对键和值同时变换(as 子句重映射)。
- 模板字面量类型用于生成字符串模式的类型约束。
- TypeScript 类型系统是图灵完备的。
- 分布式条件类型只在联合类型的裸类型参数上生效。
- [T] extends [any] 包裹可阻止条件类型分发。
项目落地视角
- 为 API 响应定义精确的类型,利用工具类型派生。
- 使用 branded type 区分相同基础类型的不同含义(UserId vs OrderId)。
- 避免过度复杂的类型体操影响代码可读性。
- 建立 utils/types.ts 统一管理项目级工具类型。
- 使用 zod 或 io-ts 实现运行时类型验证,与 TypeScript 类型互补。
常见误区
- 用 any 绕过类型检查而不是正确建模。
- 过度使用泛型约束导致类型定义难以理解。
- 忽略 strict 模式下的类型检查。
- 在递归类型中忘记处理基础情况导致无限递归。
- 混淆类型断言(as)和类型守卫(instanceof / in)。
进阶路线
- 学习 type-challenges 刷题巩固类型编程能力。
- 研究框架(Vue/React)的 TypeScript 类型设计。
- 了解 TypeScript 5.x 的装饰器和 const 类型参数。
- 学习 satisfies 运算符和 using 关键字等新特性。
适用场景
- 库和框架开发需要精确的类型定义。
- API 类型安全需要从后端同步类型。
- 复杂业务对象的类型建模。
- 组件 Props 和 State 的精确约束。
落地建议
- 建立项目级工具类型库(utils/types.ts)。
- 使用 zod 或 io-ts 实现运行时类型验证。
- 开启 strict 模式并逐步修复类型错误。
- 复杂类型添加注释说明设计意图。
排错清单
- 使用 TS Playground 排查复杂类型问题。
- 检查泛型约束是否过于宽松或严格。
- 确认类型推导结果符合预期(使用 hover 查看)。
- 检查条件类型是否意外分发。
- 确认递归类型有正确的基础情况。
复盘问题
- 项目中 any 的使用比例是多少?能否逐步消除?
- 高级类型是否让代码更安全还是更难理解?
- 如何保持前后端 API 类型的一致性?
- 类型体操是否影响了新成员的代码阅读体验?
