TS 类型体操合集

基本姿势

keyof

keyof 返回一个类型的所有 key 的联合类型:

type KEYS = keyof {
    a: string,
    b: number
} // a|b

类型索引

类型索引可以通过 key 来获取对应 value 的类型:

type Value = {a: string, b: number}['a'] // string

特别的,使用 array[number] 可以获取数组/元组中所有值类型的联合类型:

type Values = ['a', 'b', 'c'][number] // 'a'|'b'|'c'

in 操作符与类型映射

in 操作符有点类似于值操作中的 for in 操作,可以遍历联合类型,结合类型索引可以从一个类型中生成一个新的类型:

// 从 T 中 pick 出一个或多个 key 组成新的类型
type MyPick<T, S extends keyof T> = {
    [R in S] : T[R]
} 
type PartType = MyPick<{a: string, b: number, c: number}, 'a'|'b'> // {a: string, b: number}

同样,数组类型也可以遍历,R in keyof T 的结果为数组的下标:

type ArrayIndex<T extends any[]> = {
    [R in keyof T]: R
}
// 的到一个数组下标组成的新数组类型
type Indexes = ArrayIndex<['a', 'b', 'c']> // ['0', '1', '2']

extends

extends 类型于值运算符中的三元表达式:

S extends T ? K : V

若 S 兼容 T 则返回类型 K 否则返回类型 V,例如:

type Whether = "a" extends "a"|"b" ? true : false // true
type Whether2 = {a: string} extends {b: string} ? true : false // false

extends 中有一个重要的概念为类型分发,例如:

type Filter<T, S> = T extends S ? never : T
type X = Filter<'a'|'b'|'c', 'c'> // 'a'|'b'

从直观上来看 Filter 的作用是计算 'a'|'b'|'c' extends 'c' 这个表达式显然不成立,应该返回 never。但是实际上返回了 'a'|'b'。这是由于当 extends 需要检测的类型为泛型联合类型时,会将联合类型中的每一个类型分别进行检测。因此 'a'|'b'|'c' extends 'c' 实际等价于:

'a' extends 'c' ? never : T | 'b' extends 'c' ? never : T | 'c' extends 'c' ? never : T 
  = 'a' | 'b' | never 
  = 'a' | 'b' 

这里也包含了另外一个知识点,xxx|never=xxx。可以将联合类型与 extends 结合使用达到循环的效果。如果要阻止类型分发,只需要在外面套一个数组即可:

type Filter<T, S> = T extends S ? never : T
type X = Filter<'a'|'b'|'c', 'c'> // 'a'|'b'

type Filter<T, S> = [T] extends [S] ? never : T
type X = Filter<'a'|'b'|'c', 'c'> // never

如果很多时候我们既需要类型分发后的类型,还需要类型分发前的联合类型。例如如果我们判断一个类型是否为联合类型,那么可以:

type IsUnion<T> = T extends T ? [Exclude<T, T>] extends [never] ? false: true : never

即如果一个类型是联合类型,那么 execlude 掉一个其中的类型后其类型不会为 never。否则就为 never。但是在 Exclude 中出现了两 T 这明显是不行的。因此可以利用 TS 的默认类型:

type IsUnion<T, R=T> = T extends any ? [Exclude<R, T>] extends [never] ? false: true : never

这种方法可以用在既需要分发后的类型也需要原始类型的情况。

此外,extends 还有另一个需要注意的地方,泛型变量无法直接与 never 比较,需要套一个数组,例如:

type IsNever<T> = T extends never ? true: false
type Y = IsNever<never> // never

type IsNever<T> = [T] extends [never] ? true: false
type Y = IsNever<never> // true

infer

infer 可以类比到值元算的类型匹配,在类型体操中有非常多的应用。例如对于 scala:

a match {
case Success(val) => val
case _ => None
}

当 a 值为 Success() 类型时提取其中的 val。利用 infer 也可以达到相同的效果:

type ExtractType<T> = T extends {a: infer R} ? R : never // 匹配成功返回 R 否则返回 never
ExtractType<{a: {b: string}}> // {b: string}

可以看出先定义了一个模板 {a: infer R} 然后用于匹配类型 {a: {b: string}},这时就可以得到 R = {b: string}。目前 infer 出来的类型仅能应用到 extends 的成功分支
infer 也可以用匹配字面量的类型,例如:

type Startswith<T, S extends string> = T extends `${S}${infer R}` ? true : false
Startswith<"hello world", "hello"> // true
Startswith<"hello world", "world"> // false

type Strip<T, S extends string> = T extends `${S}${infer R}` ? R : T
type Y1 = Strip<"hello world", "hello "> // world
type Y2 = Strip<"hello world", "world"> // hello world

数组/元组类型

数组类型可以使用 ... 操作符进行展开:

type Add<S extends any[], R> = [...S, R]
type Y3 = Add<[1, 2, 3], 4> // [1, 2, 3, 4]

元组表示不可修改的数组,可以使用 as const 将数组转换为元组。

const array1 = [1, 2, 3, 4]
type X1 = typeof array1 // number[]
type X2 = X1[number] // number

const array2 = [1, 2, 3, 4] as const
type Y1 = typeof array2 // readonly [1, 2, 3, 4]
type Y2 = Y1[number] // 1|2|3|4

递归类型

在 typescript 类型操作符中不存在循环表达式,但是可以使用递归来进行循环操作,例如:

type TrimLeft<T extends string> = T extends ` ${infer R}`? TrimLeft<R>: T
type Y7 = TrimLeft<'  Hello World  '> // Hello World  

type Concat<S extends any[]> = S extends [infer R, ...infer Y] ? `${R & string}${Concat<Y>}` : ''
type Y6 = Concat<['1', '2', '3']>

type Join<S extends any[], T extends string> = S extends [infer R, ...infer Y] ? 
                                                (Y['length'] extends 0 ? R: `${R & string}${T}${Join<Y, T>}`)  : ''
                                               
type Y4 = Join<['1', '2', '3'], '-'> // '1-2-3'


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

推荐阅读更多精彩内容