前端精读周刊:TypeScript高级技巧实战
【免费下载链接】weekly 前端精读周刊。帮你理解最前沿、实用的技术。 项目地址: https://gitcode.com/GitHub_Trending/we/weekly
在现代前端开发中,TypeScript(简称TS)已成为提升代码质量和开发效率的重要工具。然而,许多开发者在使用TS时仅停留在基础类型定义阶段,未能充分发挥其强大的类型系统能力。本文将从实际应用场景出发,结合前端精读周刊中的经典案例,介绍5个能立即提升开发效率的TypeScript高级技巧,帮助你写出更优雅、更健壮的类型定义。
一、泛型工具类型:从Pick到Omit的组合艺术
TypeScript内置了许多实用的泛型工具类型,但理解其实现原理能帮助我们应对更复杂的场景。以Pick<T, K>和Omit<T, K>为例,这两个工具类型在日常开发中使用频率极高,它们的实现充分体现了TS类型系统的灵活性。
Pick<T, K>:精准筛选对象属性
Pick<T, K>用于从对象类型T中筛选出属性K,其实现代码如下:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
这个实现看似简单,却包含了TS类型编程的核心思想:通过keyof T获取T的所有属性名,再通过K extends keyof T约束K必须是T的属性子集,最后使用映射类型[P in K]遍历K并构建新的对象类型。
Omit<T, K>:排除不需要的属性
与Pick相反,Omit<T, K>用于排除T中的属性K。实现这个工具类型需要结合Pick和Exclude:
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
// Exclude的实现
type Exclude<T, U> = T extends U ? never : T
这里的关键是Exclude<keyof T, K>,它会返回所有属于T但不属于K的属性名,再将这些属性名传给Pick就实现了排除功能。
实战组合:Readonly2<T, K>
结合Pick、Omit和Readonly,我们可以创建一个更灵活的工具类型Readonly2<T, K>,它只将指定属性K设为只读:
type MyReadonly2<T, K extends keyof T> = Readonly<Pick<T, K>> & Omit<T, K>
这个实现将对象一分为二:先用Pick<T, K>选出需要设为只读的属性并应用Readonly,再用Omit<T, K>选出剩余属性,最后通过交叉类型&合并两者。这种组合思想在TS类型编程中非常重要,能帮助我们构建更复杂的类型工具。
更多泛型工具类型的实现案例可以参考TS 类型体操/243.精读《Pick, Awaited, If...》.md和TS 类型体操/244.精读《Get return type, Omit, ReadOnly...》.md。
二、infer关键字:类型系统的"变量"
infer是TS中一个强大但常被低估的关键字,它允许我们在条件类型中声明一个待推断的类型变量,类似于在类型系统中定义"变量"。
ReturnType :获取函数返回值类型
最经典的infer应用是实现ReturnType<T>,用于获取函数的返回值类型:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
这里的infer R告诉TS:如果T是一个函数,就推断其返回值类型并命名为R,然后返回R;否则返回never。
Awaited :处理Promise类型
infer还可以用于处理嵌套类型,比如从Promise<T>中提取T:
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer P>
? P extends Promise<unknown> ? MyAwaited<P> : P
: never
这个实现通过递归处理了嵌套Promise的情况,展示了infer与递归结合的强大能力。
实战案例:获取数组第一项类型
利用infer,我们可以轻松实现获取数组第一项类型的工具:
type First<T extends any[]> = T extends [infer P, ...infer Rest] ? P : never
这里[infer P, ...infer Rest]匹配数组类型,将第一项推断为P,剩余部分推断为Rest,然后返回P。这种模式在处理元组类型时特别有用。
关于infer的更多用法,可以参考前沿技术/207.精读《Typescript infer 关键字》.md。
三、泛型约束与类型收窄:提升类型安全性
在使用泛型时,适当的约束和类型收窄能显著提升代码的类型安全性和开发体验。
泛型约束:extends关键字
通过extends关键字,我们可以约束泛型参数必须满足特定条件。例如,React的lazy函数就使用了泛型约束:
function lazy<T extends ComponentType<any>>(
factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;
这里T extends ComponentType<any>确保了T必须是React组件类型,从而保证了lazy函数的参数和返回值类型的一致性。
类型收窄:is关键字
使用is关键字可以创建自定义类型守卫,帮助TS在条件分支中收窄类型范围。例如React的isValidElement函数:
function isValidElement<P>(
object: {} | null | undefined
): object is ReactElement<P>;
当我们在条件中使用这个函数时,TS会自动将变量类型收窄为ReactElement<P>:
const element: string | ReactElement = "";
if (isValidElement(element)) {
// element类型被收窄为ReactElement
console.log(element.props);
} else {
// element类型被收窄为string
console.log(element.length);
}
这种技巧在处理联合类型时特别有用,能有效避免"类型断言泛滥"的问题。
更多类型收窄技巧可以参考前沿技术/147. 精读《@types react 值得注意的 TS 技巧》.md。
四、类型递归:处理深层嵌套结构
TS类型系统支持递归,这使得我们可以处理任意深层的嵌套结构。一个典型应用是实现DeepReadonly<T>:
DeepReadonly :递归只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends Object ? DeepReadonly<T[K]> : T[K]
}
这个实现会递归遍历T的所有属性,如果属性值是对象类型,就对其应用DeepReadonly,否则直接设为只读。使用这个工具类型,我们可以轻松创建一个完全只读的嵌套对象类型:
type X = {
x: {
a: 1
b: 'hi'
}
y: 'hey'
}
type ReadonlyX = DeepReadonly<X>
// {
// readonly x: {
// readonly a: 1
// readonly b: 'hi'
// }
// readonly y: 'hey'
// }
元组递归:Last 和Pop
递归也可以用于处理元组类型。例如,获取元组最后一项的类型:
type Last<T extends any[]> = T extends [...infer Q, infer P] ? P : never
这里[...infer Q, infer P]会匹配任意长度的元组,将最后一项推断为P并返回。类似地,我们可以实现Pop<T>来移除元组的最后一项:
type Pop<T extends any[]> = T extends [...infer Q, infer P] ? Q : never
这些递归类型在处理状态管理和数据转换时非常有用,能帮助我们编写更通用的类型定义。
五、链式调用类型:Chainable Options
在设计API时,链式调用是一种常见模式。TS的泛型和递归类型可以帮助我们为这种模式提供完美的类型支持。
Chainable类型实现
type Chainable<Result = {}> = {
option: <K extends string, V>(key: K, value: V) => Chainable<Result & { [P in K]: V }>
get: () => Result
}
这个实现中,Chainable接收一个泛型参数Result,默认为空对象。option方法接收key和value,返回一个新的Chainable实例,其Result类型是原Result与新属性{ [K]: V }的交叉类型。get方法则返回最终的Result类型。
使用示例
declare const config: Chainable
const result = config
.option('foo', 123)
.option('name', 'type-challenges')
.option('bar', { value: 'Hello World' })
.get()
// result的类型为:
interface Result {
foo: number
name: string
bar: {
value: string
}
}
这种类型设计使得每一次链式调用都能精确推断出新的类型,为开发者提供即时的类型反馈。
总结与进阶
本文介绍的TypeScript高级技巧涵盖了泛型工具类型、infer关键字、类型收窄、递归类型和链式调用类型等核心概念。这些技巧不仅能帮助你写出更优雅的类型定义,还能让你深入理解TS类型系统的工作原理。
要真正掌握这些技巧,建议结合实际项目进行练习。你可以从TS 类型体操目录中的题目开始,逐步提升自己的类型编程能力。同时,阅读优秀开源项目(如@types/react)的类型定义源码也是提升的好方法。
TypeScript的类型系统博大精深,本文介绍的只是冰山一角。随着TS的不断发展,新的特性和技巧也在不断涌现。保持学习的热情,持续关注TypeScript的最新动态,你就能在前端开发中充分发挥TS的威力,编写出更健壮、更易维护的代码。
如果你想深入学习TypeScript,可以关注前端精读周刊,每周都有精选的前端技术文章和实战案例分享。
【免费下载链接】weekly 前端精读周刊。帮你理解最前沿、实用的技术。 项目地址: https://gitcode.com/GitHub_Trending/we/weekly
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



