类型体操之常用语法

书接上文,工欲善其事,必先利其器。在开始类型体操前,我们至少得掌握最基本的ts语法。本文将介绍一些常用的类型体操语法,包括类型映射、类型条件判断、类型推断等。掌握了这些基本语法,我们就可以解决easy部分的题目了。

类型映射

类型映射(mapping)指的是,通过某种工具类型,将一个现有的类型 A 转换成一个新的类型 B,通常用于 object 对象上。例如,将类型 A 中的所有属性都转成 string,形成新的一个类型 B,如下所示:

type A = {
  a: number;
  b: number;
};

// How to convert A to B?

type B = {
  a: string;
  b: string;
};

不卖官子,直接给出答案:

type NumberToString<T> = {
  [K in keyof T]: string;
}

type B = NumberToString<A>;
// type B = {
//   a: string;
//   b: string;
// }

这里简单介绍一下语法点:

  • T:泛型,表示任意类型。
  • K:属性名变量。
  • keyof T:返回类型 T 的每一个属性名,组成一个联合类型。这里就是 'a' | 'b'
  • in:运算符,遍历右侧的联合类型的每一个成员。
  • [K in keyof T]: string:表示遍历类型 T 的每一个属性,将属性值都转成 string 类型。

extends 关键词

这里科普一下 extends 相关的八股文,extends 有三种基本用法:

  1. 继承
interface Vehicle {
    wheels: number;
    maker?: string;
}

interface Car extends Vehicle {
    power: "gas" | "electricity";
}

接口继承接口,这个和 Java 里的用法相似,日常开发中很常见,但是类型体操里不常用。

  1. 范型约束

范型约束其实也是 Java 中常见的技巧,但是很多 java 程序员可能 10 年都没真写过范型。但是如果要成文真正的中高级程序员,比如你是写开源库的程序员,你必定大量实用范型约束。

还是回到 ts 类型系统里,例如,如果你想从对象的属性中获取值,你可能会写下以下代码:

const testObj = { x: 10, y: "Hello", z: true };

function getProperty<T>(obj: T, key: keyof T) {
  return obj[key];
}

虽然上面这段代码里给 key 加了个 keyof T的约束,但是还是不够严谨,让我们看一下 getProperty函数的调用:

const xValue = getProperty(testObj, 'x');
// const xValue: string | number | boolean

const yValue = getProperty(testObj, 'y');
// const yValue: string | number | boolean

getProperty函数的返回值类型是 string | number | boolean,这显然不是我们想要的。我们希望的是: xValue: number, yValue: string。那么,我们怎么做到这一点呢?答案就是:范型约束 + 条件类型。

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const xValue = getProperty(testObj, 'x');
// const xValue: number

const yValue = getProperty(testObj, 'y');
// const yValue: string

在上述示例代码中,使用关键字 extends 来约束泛型类型K。换句话说,选择T的键之一作为K,而不是允许任何键作为键参数的类型。因此,函数可以限制为仅返回一种类型,而不是联合类型。

范型约束在类型体操中通常用于判定 @ts-expect-error,类似于预期抛错的单测,大约一半的题目会用到。

  1. 条件类型

type 运算中没有 if 关键词;我们做条件判断时,只能依赖三元运算:T extends U ? X : Y。如下所示:

type IsString<T> = T extends string ? true : false;

type x = IsString<'hello'>; // type x = true
type y = IsString<number>; // type y = false
type z = IsString<string>; // type z = true

逐字解释一下 T extends string ? true : false;

如果 T(左边)的类型是 string(右边)的子类型,也就是说,如果T类型可以赋值给 string,那么返回 true; 否则,返回 false。

三元运算在类型体操中非常实用,几乎每一道题目都能见到它。

整数

这里的整数不是指 number 类型,而是number的所有整数子类型,比如:type N = 0 | 1 | 2。类型体操中是有针对整数操作的题目的,但是很可惜类型系统中没有加减乘除四则运算,因此解这类题目只能借助其他类型来模拟。

类型系统里最直接获得整数结果的方法就是获得元祖(tuple)的长度,比如:

type Length<T extends any[]> = T['length'];
type five = Length<[1, 2, 3, 4, 5]>; // 5

这里用到了上文提到的 extends 范型约束,T必须是一个元祖类型,否则会报错。然后,通过元祖的 length 属性,就可以获得元祖的长度,也就是整数。

p.s. 读取类型的属性用的方括号语法,这个和JS有共通之处

type Tuple = [1,2]

type Len = Tuple['length'] // Len = 2
type Idx = Tuple[0] // Idx = 1

那么,假如需要实现整数相加呢?本质上就是通过拼接两个元祖来来得到它们的总长度:

type Add<A extends any[], B extends any[]> = [...A, ...B]['length']

type Test = Add<[1, 2, 3], [4, 5, 6]> // 6

如上所示,拼接元祖用到的是 ... 扩展运算符,这个和JS里的数组扩展运算符也是一样的。
但是这里还是提醒一下,JS语法和类型系统语法差距很大,只有极少数相同,不能混用。比如不能在类型体操中直接使用 Array 类型,也不能使用 pushpop 等数组方法。

递归

Typescript 类型系统里还有一个反常规的特性:就是没有 for 循环。很多人可能会不适应,但事实上当你接触的编程语言变多了,就会发现,很多正经的语言它就是没有 for 循环的,比如 Haskell;即便是在react,也不推荐使用 for。理由很简单for循环本质上就是产生side effect。

言归正传,在做题时,保证会碰到需要使用跌倒的造作,没有for,那怎么实现循环呢?
只能是递归了。(数学上已经证明迭代都可以用递归实现,这里不再展开了)

递归的思路很简单,就是将问题拆解成更小的子问题,然后通过递归调用自身来解决问题。比如,实现一个整数加一的递归函数:

type addOne<T extends number, R extends any[] = []> = R['length'] extends T 
    ? [...R, 1]['length']
    : addOne<T, [...R, 1]>

type Test2 = addOne<5> // 6

这道题目对初学者来说有点难了,用到了我们上面提到的所有语法,但仔细看,其实思路很简单:

  1. 定义一个递归函数 addOne,它接受两个参数:TR
  2. T 是目标整数,R 是一个元祖,用于存储递归过程中产生的中间结果。
  3. 在递归函数中,首先判断 R 的长度是否等于 T,如果等于,则返回 R 的长度加一,否则继续递归调用 addOne 函数,并将 R 的长度加一作为新的参数传入。

实现递归操作,我们用到一点小技巧:

  1. 一般都要添加一个默认参数 R,做递归结束判断。
  2. 给R一个默认值 (如 [], 这样就可以在递归调用时,直接省略初始化操作

infer

infer 是 Typescript 类型体操中一个非常重要的关键字,它用于在类型体操中推断类型。它的使用方式是在类型体操的函数中,使用 infer 关键字来声明一个类型变量,然后在类型体操的函数中,使用这个类型变量来推断类型。

比如,实现一个类型体操函数,用于推断元祖的最后一个元素:

type Last<T extends any[]> = T extends [...infer R, infer L] ? L : never

type Test3 = Last<[1, 2, 3]> // 3

infer还有很多技巧,这里就不展开了,我们会在后续的文章中,在各个案例中详细展开介绍。

总结

本文介绍了开启 TS 类型体操前,所必备的基础知识,包括类型映射、条件判断、递归、类型推导等。这些知识是 TS 类型体操的基础,掌握了这些知识,我们就可以开始进行 TS 类型体操了。

最后,在加一些自己的心得:TS 类型系统事实上是和JS完全不同的一种语言。大家要以一种学习新语言的心态来做专项训练,不要试图用JS的语法去操作TS的类型体操。当你又掌握一门新语言后,你的世界会骤然开阔。

文章同步发布于an-Onion 的 Github。码字不易,欢迎点赞。

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