TypeScript: 低维护类型

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容