美文网首页
TypeScript: 低维护类型

TypeScript: 低维护类型

作者: 一蓑烟雨任平生_cui | 来源:发表于2022-02-17 16:31 被阅读0次

typescript 具有类型推断能力,所以在 typescript 中编写常规的 JavaScript 时很多类型可以推断出来,不需要明确指定类型。但有些情况下又必须要添加类型注释,随之带来维护类型的烦恼。

创建一种低维护类型,也就是该类型在他的依赖或环境发生变化时自我更新的类型。

场景 1: 信息已经可用

在 JavaScript 中经常看到如下模式。

const defaultOptions = {
  from: './src',
  to: './dest'
}

function copy(options) {
  // 合并默认选项
  const allOptions = { ...defaultOptions, ...options }

  // do something...
}

在 ts 中需要显式创建类型:

type Options = {
  from: string
  to: string
}

const defaultOptions: Options = {
  from: './src',
  to: './dest'
}

type PartialOptions = {
  from?: string
  to?: string
}

function copy(options: PartialOptions) {
  // 合并默认选项
  const allOptions = { ...defaultOptions, ...options }

  // do something...
}

这种是我们最常使用的方式。但是,假设向 Options 中添加一个字段时就不得不改动三处代码:

type Options = {
  from: string
  to: string
  overwrite: boolean
}

const defaultOptions: Options = {
  from: './src',
  to: './dest',
  overwrite: true
}

type PartialOptions = {
  from?: string
  to?: string
  overwrite?: boolean
}

其实defaultOptions提供给我们的信息已经够用了。下面进行优化:

  1. 使用内置类型Partial<T>来获得与PartialOptions类型相同的效果
  2. 利用 typeof运算符动态创建新类型
const defaultOptions = {
  from: './src',
  to: './dest',
  overwrite: true
}

function copy(options: PartialOptions) {
  // 合并默认选项
  const allOptions = { ...defaultOptions, ...options }

  // do something...
}

优势:

  • 如果添加新字段,完全不需要维护其他东西
  • 只有一个单一的信息来源:defaultOptions 对象,这是运行时拥有的唯一信息
  • 不仅代码简洁,ts 也不具有很强的侵入性,更符合 js 的编写方式

类似的例子:使用 consttyoeof 运算符可以将元组转为联合类型。

const categories = ['beginner', 'intermediate', 'advanced'] as const

// "beginner" | "intermediate" | "advanced"
type Category = typeof categories[number]

同样,我们只维护一个 categories,即实际数据。转换 categories 为元组类型并对每个元素进行索引。

场景 2:关联模型

在大多数情况下,明确地处理类型和数据是有意义的。如下:

type ToyBase = {
  name: string
  price: number
  quantity: number
  minimumAge: number
}

type BoardGame = ToyBase & {
  kind: 'boardgame'
  players: number
}

type Puzzle = ToyBase & {
  kind: 'puzzle'
  pieces: number
}

type Doll = ToyBase & {
  kind: 'doll'
  material: 'plastic' | 'plush'
}

type Toy = BoardGame | Puzzle | Doll

ToyBase 类型拥有 BoardGame、 Puzzle、 Doll 共有的属性,这三个类型又都拥有值不相同的 kind 属性。Toy 为三个类型的联合类型。

通过一下方式获取某个类型的 kind

function printToy(toy: Toy) {
  switch (toy.kind) {
    case 'boardgame':
      // todo
      break
    case 'puzzle':
      // todo
      break
    case 'doll':
      // todo
      break
    default:
      console.log(toy)
  }
}

如果需要基于这些数据创建更多的类型,比如:

type ToyKind = 'boardgame' | 'puzzle' | 'doll'

type GroupedToys = {
  boardgame: Toy[]
  puzzle: Toy[]
  doll: Toy[]
}

如果要基于 ToyBase 添加一个新类型VideoGame

type VideoGame = ToyBase & {
  kind: 'videogame'
  system: 'NES' | 'SNES' | 'Mega Drive' | 'There are no more consoles'
}

这时候又必须修改三个地方:

type Toy = BoardGame | Puzzle | Doll | VideoGame

type ToyKind = 'boardgame' | 'puzzle' | 'doll' | 'videogame'

type GroupedToys = {
  boardgame: Toy[]
  puzzle: Toy[]
  doll: Toy[]
  videogame: Toy[]
}

这样大量的维护不仅繁琐,更容易出现拼写错误。可以通过 ts 的内置类型进行优化。

首先通过直接访问类型的方式创建一个包含所有 kind 类型组成的联合类型。

type ToyKind = Toy['kind']

然后使用映射类型创建 GroupedToys

type GroupedToys = {
  [Kind in ToyKind]: Toy[]
}

这样当 Toy 类型改变时 ToyKindGroupedToys 会自动进行维护。

还可以进一步优化,先了解下内置类型 Extract<T, U>,该类型是提取联合类型 T 和联合类型 U 的所有交集。通俗地说:从联合类型中提取指定的类型。

type GetKind<Group, Kind> = Extract<Group, { kind: Kind }>

type DebugOne = GetKind<Toy, 'doll'> // DebugOne = Doll
type DebugTwo = GetKind<Toy, 'puzzle'> // DebugTwo = Puzzle

应用于 GroupedToys

type GroupedToys = {
  [Kind in ToyKind]: Extract<Toy, { kind: Kind }>[]
}

// 等价于

type GroupedToys = {
  boardgame: BoardGame[]
  puzzle: Puzzle[]
  doll: Doll[]
}

GroupedToys 的属性应该是复数,通过类型断言增加 s

type GroupedToys = {
  [Kind in ToyKind as `${Kind}s`]: Extract<Toy, { kind: Kind }>[]
}

// 等价于

type GroupedToys = {
  boardgames: BoardGame[]
  puzzles: Puzzle[]
  dolls: Doll[]
}

总结:

创建低维护类型的方法:

  1. 为你的数据建模或从现有模型中推断
  2. 定义派生类(映射类型、Partials 等)
  3. 定义行为(条件)

本文摘录自:TypeScript in 50 Lessons

相关文章

网友评论

      本文标题:TypeScript: 低维护类型

      本文链接:https://www.haomeiwen.com/subject/yijslrtx.html