前端精读周刊:TypeScript高级技巧实战

前端精读周刊:TypeScript高级技巧实战

【免费下载链接】weekly 前端精读周刊。帮你理解最前沿、实用的技术。 【免费下载链接】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。实现这个工具类型需要结合PickExclude

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>

结合PickOmitReadonly,我们可以创建一个更灵活的工具类型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...》.mdTS 类型体操/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 前端精读周刊。帮你理解最前沿、实用的技术。 【免费下载链接】weekly 项目地址: https://gitcode.com/GitHub_Trending/we/weekly

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值