TypeScript的索引类型与映射类型,以及常用工具泛型的实现

相信现在很多小伙伴都在使用 TypeScript(以下简称 TS),在 TS 中除了一些常用的基本类型外,还有一些稍微高级一点的类型,这些就是我本次文章要讲的内容:索引类型与映射类型,希望小伙伴们看过这篇文章后能对 TS 有更深一步的理解。

索引类型

下面我通过一个官方的例子来说明下什么是索引类型:

function pluck(o, names) {
  return names.map((n) => o[n])
}

这是个简单的函数,names 是一个数组,里面是 key 值,我们可以从“o”里面取出这些 key 值,理想情况下 names 里面的 key 应该都是“o”里面包含的,否则最终的结果里面就会有 undefined,这个函数返回的结果也应该是“o”中都包含的 value 值,那么我们如何才能做到这些类型约束呢,如果只用一些基础类型,很难达到满意的效果,下面使用索引类型改写下:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map((n) => o[n])
}

interface Person {
  name: string
  age: number
}
let person: Person = {
  name: 'Jarid',
  age: 35
}
let strings: string[] = pluck(person, ['name']) // ok, string[]

改写后这个函数是一个泛型函数,泛型为 T 和 K,其中 K 有点特殊,K extends keyof T,是什么意思呢,其中 keyof 就是索引类型查询操作符,我们从字面意思理解,它就是 T 的 key,就是 T 上已知的公共属性名的联合,对于上面的代码,keyof Person就是'name'|'age',那么K extends keyof T就是K extends 'name'|'age',这样我们就获取到了 Person 上所有 key 组成的一个联合类型,然后参数o: T, names: K[],就很好理解了,names 就是 K 组成的一个数组。返回值中T[K][]我们需要拆开来看 T[K]和[],就是 T[K]组成的一个数组,那么 T[K]是什么类型呢,它就是索引访问操作符,类似于 js 中对象的取值操作,不过这里取的是类型,因为 K 是'name'|'age',所以 T[K]就是string|number,这些就是索引类型,其实也不难理解,下面再说下映射类型,它和索引类型结合起来可以做很多事情。

映射类型

映射类型也很容易理解,我们先看一个简单的例子

type Keys = 'option1' | 'option2'
type Flags = { [K in Keys]: boolean }

这个就是一个简单的映射类型,其中的in可以理解为是我们平时用的for...in,就是去遍历 Keys,然后把 boolean 赋给每一个 key,上面的 Flags 得到的结果就是

type Flags = {
  option1: boolean
  option2: boolean
}

很简单吧,那么这个东西有什么用处呢,请看下面的例子:

// Person
type Person {
    name: string
    age: number
}

我们想把这个 Person 里面的属性都变成只读的,像这样:

// Readonly Person
type Person {
    readonly name: string
    readonly age: number
}

如果我们有很多这样的类型,那么改起来会很麻烦,因为每次都要把这个类型重新写一遍。其实我们可以使用刚才的索引类型和映射类型来写一个泛型:

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

[P in keyof T]就是遍历 T 中的 key,T[P]就是当前的 key 的类型,其实[P in keyof T]: T[P]就是把 T 遍历了一遍,但是我们在属性前面加了个 readonly,这样我们调用这个泛型的时候,它就会把传入的类型的 key 遍历一遍,遍历的同时在前面加个 readonly,最终给我们返回一个新的类型。我们在调用的时候只需要这么用:

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}
type Person {
    name: string
    age: number
}
type ReadonlyPerson = Readonly<Person>

索引类型和映射类型除了能实现 Readonly,还能实现很多有意思的东西,我们平时在使用 TS 的时候,TS 已经内置了一些常用的辅助泛型,刚才的 Readonly 就是其一,另外还有很多,我从 TS 的类型定义文件里找了一些,这些泛型从简单到复杂的都有,但基本上都是用上面提到的两个类型实现的,下面我们一起来分析一下。

TS 常用的辅助泛型及其实现方式

首先来看第一个

/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P]
}

相信这个泛型很多人都用过,就是把类型都变成可选的,和刚才的 Readonly 是类似的实现方式,只是这个是在后面加了个问号,这样一来属性就变成可选的了。
与之相对的还有一个 Required

/**
 * Make all properties in T required
 */
type Required<T> = {
  [P in keyof T]-?: T[P]
}

注意这个稍有点不同,它是-?,其实就是减去问号,这样就可以把问号去掉,从而变成必选的属性。再来看下一个

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

如果你理解了最开始的那个 pluck 函数,这个就很好理解了,我们传入 T 和 K,其中 K 是 T 的 keys 组成的联合类型,再看返回值[P in K]: T[P],就是把 K 遍历了一遍,同时赋值上原类型,那么综合来看 Pick 就是帮我们提取出某些类型的,比如通过Pick<Person, 'name'>我们就可以得到{name: string},再来看下一个

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T

这个泛型传入一个 T 和 U,然后它判断了 T 是否属于 U,属于的话返回 never 否则返回原类型 T,注意 never 在最终的类型中是不会存在的,所以它可以帮助我们消除某些属性,其实这个 Exclude 就是消除了T extends U的类型,比如我们使用Exclude<'a'|'b','b'|'c'>,最终会得到'a',与之相反的有:

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never

这个正好相反,是从 T 中取出 U 中拥有的类型。

有了 Exclude,我们就可以和 Pick 结合来实现另外一个:

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

这个泛型是先使用了Exclude<keyof T, K>,去除了 keyof T 中的 K,然后又使用 Pick 取出了这些类型,这样我们就可以从 T 中去除 K 里面包含的 keys 了,达到和 Pick 相反的效果。

我们再来看另一个稍微复杂一点的

type NonNullObject<O> = Pick<
  O,
  {
    [K in keyof O]: O[K] extends null | undefined ? never : K
  }[keyof O]
>

这个不是 TS 内置的类型,但也是一个很有用的类型,我们来一点一点分析。首先这个泛型使用了 Pick,我们知道 Pick 就是取出一些属性,我们先看传给 Pick 的第二个参数


{
  [K in keyof O]: O[K] extends null | undefined ? never : K
}[keyof O]

它遍历了 O 的 keys,然后进行了一个判断,如果是extends null | undefined则返回 never,否则返回 K,K 就是 O 中的 key 值,注意这里和之前的一些泛型有些不一样,之前的都是O[K],而这里的属性的值还是 K,最终我们得到的是类似K:K这样的东西,比如{name: string, age: null}这个,经过上面的转化会变成{name:'name', age:never},可能有些小伙伴还不清楚为什么要这样转换,我们接着往下分析,经过这个转换之后,又进行了一个操作[keyof O],对于 Person,keyof O 就是'name'|'age',那么这里就就是{name:'name', age:never}['name'|'age'],这样就很清晰了,其实就是一个取值操作,这样我们就可以得到'name'|never,还记得 never 的特性吗,它可以帮我们消除一些类型,那么最终的就是'name',这也是为什么我们写成类似 K:K 这样,就是要把 null|undefined 对应的 key 转换成 never,然后再通过 keyof 把他们全都取出来,别忘了最外面还有一个 Pick,这样我们就从原始类型中去除了 null|undefined。

另外还有一个比较有用的是 ReturnType

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any

它可以帮我们取到函数返回值的类型,这个 ReturnType 接收的一个参数是函数,然后进行了一个判断T extends (...args: any) => infer R,就是判断是否是函数,这里有个东西是 infer,通过这个操作符我们可以获取 R 的引用,就是函数的返回值,最终再把 R 返回出去,就获得了函数 T 的返回值。

其实除了我分析的这些泛型,TS 还内置了其他的很多泛型,比如还有获取函数的参数的,获取构造函数类型的,总的来说各种泛型基本上都可以用索引类型和映射类型实现,希望大家看过这篇文章后能多多使用这两种类型,在自己的项目里也能开发一些常用的辅助泛型,来提升工作效率。

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

推荐阅读更多精彩内容