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
提供给我们的信息已经够用了。下面进行优化:
- 使用内置类型
Partial<T>
来获得与PartialOptions
类型相同的效果 - 利用
typeof
运算符动态创建新类型
const defaultOptions = {
from: './src',
to: './dest',
overwrite: true
}
function copy(options: PartialOptions) {
// 合并默认选项
const allOptions = { ...defaultOptions, ...options }
// do something...
}
优势:
- 如果添加新字段,完全不需要维护其他东西
- 只有一个单一的信息来源:
defaultOptions
对象,这是运行时拥有的唯一信息 - 不仅代码简洁,ts 也不具有很强的侵入性,更符合 js 的编写方式
类似的例子:使用 const
和 tyoeof
运算符可以将元组转为联合类型。
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 类型改变时 ToyKind
和 GroupedToys
会自动进行维护。
还可以进一步优化,先了解下内置类型 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[]
}
总结:
创建低维护类型的方法:
- 为你的数据建模或从现有模型中推断
- 定义派生类(映射类型、Partials 等)
- 定义行为(条件)
本文摘录自:TypeScript in 50 Lessons