TypeScript 类型体操指北

核心提示最近公司组织技术分享,作为一个懒癌患者,终于可以更新一下了前言「类型体操」一词,最早出现在 2006 年 Haskell 的文档。本文需要读者有一定的 TypeScript 使用经验、熟悉 TypeScript 官方文档。本文旨在帮助开发者

最近公司组织技术分享,作为一个懒癌患者,终于可以更新一下了
前言
  • 「类型体操」一词,最早出现在 2006 年 Haskell 的文档。
  • 本文需要读者有一定的 Typescript 使用经验、熟悉 Typescript 官方文档。

  • 本文旨在帮助开发者深入理解 Typescript 类型系统,并灵活运用「类型体操」实现各种各样类型变换的需求。

    文中将类型类比做集合,帮助初学者更好地理解「类型体操」底层逻辑。但类型本质上并不等价于集合,在某些细节上必然存在出入,需要开发者根据实际情况自行甄别。如发现内容有误,欢迎指正。

  • 本文定位是「手册」,贯彻「即插即用」「看的到抄的走」的理念,故不会涉及太多理论性内容的讲解。

  • 目前各大知识分享社区关于「类型体操」已经很多优秀的文章,如:Typescript 类型体操天花板,用类型运算写一个 Lisp 解释器、使用Typescript类型体操实现一个简易版扫雷游戏 等。但这些文章存在一定的阅读门槛,也很难「抄的走」,故本文期望能降低门槛,补齐知识盲区,梳理知识结构,让更多人有志之士能掌握这一技能。
  • 类型体操虽好,可不能贪杯啊!(具体业务场景中是否适用,还请自行斟酌)
  • 本文是【万字长文】深入理解 Typescript 高级用法 内容补充,如发现部分内容理解困难,可与上述文章一起食用。

常见类型概览常见类型列举基础类型string一般指是字符串类型的总称例如:"Hello, world"模板字符串类型模板字符串类型 最早从 TS 4.1 版本开始出现,并在后续版本中不断增强。 一般为如下形式:type World = "world";type Greeting = `hello ${World}`;number一般指数字类型的总称,包含 intfloatboolean一般指 truefalse 的总称复合类型通用 K/V 结构一般形如下述结构:// interfaceinterface MyKVStructure {[key in string | number | symbol]: any;}// type aliastype MyKVStructure = {[key in string | number | symbol]: any;}显然 通用 K/V 结构 包含两个可供设置类型的「槽位」[^5],其中「键」类型只能为stringnumbersymbol,而「值」类型理论上可以为任何类型。

补充阅读:Differences Between Type Aliases and InterfacesArray、Tuple
  • Array 本质上只是特殊形式的 K/V 结构,常用声明方式为 Array、或 string[];
  • Tuple 是特殊形式的数组,详见typescript handbook#Tuple Types
泛型「泛型」一般指的是在程序编码中一些包含类型参数的类型 例:// 这里的 T 就是类型参数interface MyType {a: T}在某些简单的场景下,我们也可以把它作为「类型模板」来用函数类型常见的形如 => R 的结构 函数有如何几种常见声明方式:

type

MyFunction

=

=>

void

;

interface

MyFunction

{

:

void

;

}

type

MyFunction

=

{

:

void

;

}

各类型常见场景基础类型常见场景stringnumberboolean 这类基础类型常用于「声明变量」Array、Tuple
  • Array 常见于数组相关类型的描述
  • Tuple 作为 Array 类型的「子集」在日常开发场景中并不多见,通常大家都会选择使用 Array 类型来实现描述数组类型的描述
  • Tuple 有一个专用场景:用于描述「函数参数」类型,故 TS 4.0 版本以后新增了 Labeled Tuple 功能支持 详见官方文档
  • Array 提供了一个可供设置类型的槽位「槽位」[^5],如果设置多个,只能使用 Union Type,而 Tuple 提供了多个可供设置类型的「槽位」[^5],可以清晰、有序、准确地描述一组/多组类型,故在进行多个「类型推导」时,常使用 Tuple 存储 输入值/中转值。

Function
  • Function 类型常用于函数类型描述
  • Function 类型有两个可以 设置类型 的「槽位」,分别为:「参数类型」、「返回值类型」。
  • Function 类型的「参数类型」是「双向协变」的,而「返回值类型」是「协变」的,可以利用此特性实现类型逻辑的「反转」与「映射」。
结构化类型系统读完上述内容后,部分读者会好奇为何文章要强调 K/V 结构,而非直接用 object 来代指。

实际上 Typescript 遵循的是「结构化类型」规范,也就是人们常说的「鸭子类型」。 换言之,针对 K/V 结构类型,只要满足「给定结构兼容」,那么就可以判定二者兼容。详见 typescript handbook structural-type-systemSubtypeAssignment在官网的解释中,Typescript 存在两种「类型兼容」方式:SubtypeAssignment

... In Typescript, there are two kinds of compatibility: subtype and assignment ...
Assignment 详细规则见下表: Ts与集合名词类比对照表
名词集合Typescript
包含于extends
交集&
并集|
空集never
全集Uunknown
类型推导基本运算集合运算A B使用 extends 关键字 例:type TestUnknown = T extends unknown 'Y' : 'N';type TestNever = T extends never 'Y' : 'N';// 任何集合都包含于全集TestUnknown; // Y// 任何集合都不包含于空集TestNever; // N
A B使用 & 符号A ∅ = ∅例:// 任何集合与空集的交集都是空集type Test = string & never; // neverA U = A例:

// 任何集合与全集的交集都是自己

type

Test

=

string

&

unknown

;

// string

常见运用
  1. 利用 unknown 剔除「交叉类型」中某些不需要的类型
  2. 利用 never 实现「一着不慎,满盘皆输」的判断操作(例如 js 中 Array.prototype.some 的表现)
  3. | 联合使用实现批量过滤
A B使用 | 符号A U = U例:// 任何集合与全集的并集都是全集type Test = { b: number } | unknown; // unknownA ∅ = A例:// 任何集合与空集的并集都是自己type T1 = string | never; // { b: number }常见运用
  1. 剔除「联合类型」中某些不需要的类型,常见使用范例如:某些「内置范型操作符」:ExcludeExtractOmit
  2. 利用 unknown 实现「一着不慎,满盘皆输」的判断操作(例如 jsArray.prototype.some 的表现)
  3. & 联合使用实现批量过滤
条件运算在常见的编程语言中,我们常常使用 if ... else 或者「三元运算」来实现「条件运算」。

「三元运算」在常见编程语言中有一个固定的格式:条件表达式 表达式1 : 表达式2Typescript 类型系统中,我们同样使用类似「三元运算」的方式来实现「条件运算」,这被称为 Conditional Types「条件表达式」本质上最终会返回 boolean 类型值,在 Typescript 类型系统中,依然遵循此规则。在 typescript 中常常使用 例:type MyType = true extends boolean 'Y' : 'N'; // Y循环/遍历在常见的编程语言中,我们常常需要对 ArrayTuple 结构进行 循环/遍历,语言本身也内置了很多方法帮助我们完成这样的需求。但是在 Typescript 类型系统 中,并不支持这样直接地 循环/遍历 像 ArrayTuple 这样的结构。Union Type古怪 表现type MapType = { a: T };type MapTypeByConditionalType = T extends any { a: T } : never;type TT = MapType; // { a: string | number; }type TT = MapTypeByConditionalType; // { a: string } | { a: number }我们发现 Union Type 在经过某些 泛型操作符 后 "裂开了" Union Type 更像是一个「非空有序集合」下述为 type T = string | numberAST 描述:再来看一下 getIn 的入参校验:解题思路:get 方法比较简单,直接使用 function 的反向类型推导,根据入参 K ,取出返回值 T[K] 的类型


getIn 方法比 get 方法稍复杂一些,我们需要一个包含所有 key 的组合 联合类型

例如:interface Obj { a: string; b: {c: number }}上述这样一个结构,我们希望生成:["a"] | ["b", "c"] 这样的一个 联合类型 。要完成上述需求,我们需要:

  1. 递归地暴力枚举所有的 key 和 value
  2. 判断 value 如果是非嵌套类型,那么把对应的 key push 到上一次的 tuple 里,如果依然是嵌套类型,那么继续递归下去
Tips:这里为了简化代码,会用到一个作者自己封装的库 :

具体功能就是用ts的类型系统实现类似 js 原生的 api 的工具函数,有兴趣可以了解一下,这里我们会用到 push 功能来实现最终结果的收集。
getByKeyPathStr 方法就很简单了,我们只需要基于 getIn 方法,使用类似 join 的操作,把入参的 ["a"] | ["b", "c"] 转换为 "a" | "b.c" ,这里为了简化代码,还是用到了 todash 中的 join 方法。总结
  1. 这里用到了文章最初讲到的 递归、暴力枚举类型。

  2. 这里利用了联合类型的 非空、有序、唯裂开的特性。
  3. 上述代码为了利于讲解,去除了js逻辑部分,有兴趣的同学可以补齐。
  4. 上述代码中 getIngetByKeyPathStr 方法为了简化场景,只完成了入参部分的类型推导,有兴趣的同学可以补齐返回值部分。

  5. 入参类型推导其实还有缺陷,实际应为 ["a"] | ["b"] | ["b", "c"] ,但改动其实不大,有兴趣的同学可以修正一下。
Q&A为什么同样是 declare 声明的类型,有时在全局生效,有些只能在局部生效?
  1. 首先确保你自定义的类型均已被 ts 加载
  2. 检查你所编写的内容属于「脚本」还是属于「模块」,常见区分方法为:使用了 importexport 关键字则为「模块」。详见「typescript handbook」#modules
  3. 如果是「脚本」,直接 declare 即可在全局生效,若为「模块」,则仅在局部生效
【接上】「模块」内如何声明/扩展全局类型?declare global {interface String { // ...}}详见「typescript handbook」#global modifying module【接上】「脚本」内如何引入其他类型?// 如果是 类库/// // 如果是 自定义文件/// 详见「typescript handbook」#reference types如何扩展一个库/模块内部的类型?详见官方文档我是一个伸手党,我想拿来就用,除了官方内置的泛型操作符有没有其他现成好用的库?
  • ts-toolbelt
  • todash
为什么有些人觉得 ts 很难学/难用?常见原因: 解决方案
  1. 请阅读文档
  2. 请仔细阅读文档
  3. 请熟读并背诵文档

名词解释
  • 非空有序集合:非空:指每一项元素不为空;有序:指该列表保持有序;集合:指每一项元素唯一。

 
友情链接
鄂ICP备19019357号-22