AI summary
type
status
date
Nov 20, 2024 06:18 AM
slug
summary
tags
category
icon
password
TS 体操知识点记录

【类型】基础类型
下面罗列一些基本类型的 trick:
number/string 比较大小
P.S.
这里 string 指的也是 `${number}` 类型的 string 比较,不考虑 alphabet 拼写顺序之类的字符比较
通常比较 number,都是将 number 转换为对应大小的数组,然后比较数组之间的大小
例如 题目-4425
上面利用了
infer
的 trick,但是注意它可能具有以下问题:- 只能判断 T > U,而非 T ≥ U,等于的 case 会返回 false(不过针对这一点将 U/T 反过来就好)
- 如果number 过大,会占用较大内存,如下

因此最好的思路是转成 string,按照人为比较的思路,从高位到地位依次比较,不过写起来很复杂,我也只是写了个伪码:
主要还是,TS 这门语言就不适合写一些带
>
、 <
等比较的逻辑,这或许也不符合 TS 设计的范式,本就是静态判断的语言,为何要做动态计算?如果有比较的逻辑,应该属于运行时 JS 的部分了如何表示空对象 {}
【类型】联合Union
字符串 ⇒ 联合类型
题外话
题目 4260——获取字符串全排列,其实期望的是按照 BFS 得到的排列
但是因为TS只比较方便实现 dfs 递归,所以大部分 solutions 都得到的结果是这样
如果我就是想得到 BFS 的结果呢?关于这点 SOF 有个帖子说明,BFS 和 DFS 的核心差异是 DFS 基于
stack
数据结构,而 BFS 必须基于 queue
,因此递归天然不支持 BFS。不过也有一些 hack 的技巧可以强行实现 BFS,例如递归中传入 queue,关于这点,我在下方的 “TS图灵完备性” 章节中会进行尝试
数组/对象 ⇒ 联合类型
- 在数字后用
[number]
索引
- 在对象后用
[keyof T]
索引
例如 BEM这题,在模板字符串中,可以直接用
`${ARR[number]}`
唤起 Union 的 DisTributive 特性联合类型 ⇒ 数组/对象
其实按照 TS 的设计,不支持 Union 转 Tuple,因为 Union 是无序的,而 Tuple 是有序的
不过我们可以利用
函数重载
这一 hack 的特性实现,具体代码和说明在下方函数逆变的部分相关的体操题:
另外有一篇blog也有详细说明,解法是一样的
联合类型判断 null/undefined 等
由于联合类型存在 DisTributive 特性(参考下方),因此判断 null/undefined 等需要注意,如果不期望 DisTributive,name 需要在左右加上括号[]
但是实测发现一个 tricky 的现象(引自中序遍历题),这里 DisTributive 也应该符合预期的,但是如果用
T extends TreeNode
就不会有 ts error,但是不论是 T extends null
或者 [T] extends [null]
都会报错,原因未知,猜测为 ts lsp error个人认为最后这个解法只是利用 NT 规避掉了 ts 判断 null 的异常,没有根本解决问题,报错也不是 issue 里提到的 DisTributive 导致的(因为我上面尝试了 extends null 和 extends [null],都不 work),这里实际原因暂未排查出来
另外我发现 TS 的各种高级特性,和 TS 的版本息息相关,需要时刻关注版本更新,例如上例:
- 5.4 以下,会报错
Type instantiation is excessively deep and possibly infinite.(2589)
Expression produces a union type that is too complex to represent.(2590)
- 试了下 4.0,泛型甚至不能自身递归调用😅,没有测试具体到哪个版本才支持
至此,也进一步加深了我对 TS 的认知:TS 也是一个人为的 AST 语言转换器,所以也会存在一些 issue,我们使用和学习的时候,要保持质疑的态度,也要保持跟进最新版本进展
【类型】模板字符串(Template Literal Types)
泛型变量作为 object 的 key
通过一下 2 点实现:
T extends string
key in T
例如
有意思的是,虽然两个推导结果一致,但是 ts 无法获取 AppendToObject2 推导的结果,原因根据网上搜集的资料显示,应当是考虑 ts 推导性能,不会完全推导多个复杂 ts 泛型联合类型
高级推导
模板字符串的高级推导(配合 infer 条件推导的情况)中的一些 tricky 现象:
可以观察到
${string}
是不占位的,并且如果你看一下 `${string}${string}` 的类型,你会发现是string
- 其他所有类型会占位,包括 any、infer。占位的意思是至少要占用 1 个 char,或者空字符
''
,如果已经有类型变量(placeholder)占用了空字符''
,那其他 placeholder 不能再占用,必须匹配长度 > 1 的子串
- 基础类型只能用常量,即
${123}
而非${number}
,否则不匹配
至于原因,官方 repo 的几个 issue 也未解释清楚:
但是,基于这些 issue,我推测模板字符串的匹配算法(matching algorithm)应当如下
- 获取当前需要匹配的位置(placeholder),即
${}
内部的变量,假设是 P
- 从当前位置,左往右遍历字符串,每次尝试加入 1 个 char 作为 P 的值,如果不匹配,就继续拼接,直到第一次匹配或者走到字符串末尾
- 匹配 P 之后,继续匹配下一个模板字符串中的变量,更新变量 P 和当前位置,并重复 1,2过程
- 如果走到字符串结尾,完成匹配,直接返回结果(因此可以认为 TS 和正则不同,默认是非贪婪模式)
- 如果走到字符串结尾,未完成匹配,则
- 判断有没有 P 已经占用了空字符,如果没有,设置当前 P 为空字符(前提是 P 为 string、any 等空字符的父类类型)
- 空字符已占用,尝试弹出上一个匹配的 P,在之前匹配 P 的值基础上,继续拼接后续字符,找到更长的子串作为 P 的值
- 如果找不到,持续弹出,直到栈为空并且走到字符串结尾
至于
${string}
这个 corner case,个人认为就是 bug,原因就是除了 string 其他类型表现都相似(推导类型、匹配占位),官方后续有可能会修复该问题,个人建议在此之前就尽量避免使用 ${string}
下面举例说明
可以看到,结果是除了 test01 是 never,其他类型都是
a
我们再调整一下
现在,就是 test01,test11,test21是 never,其他值是
ab
再调整一下(可自行到 https://www.typescriptlang.org/play 查看)
可以观察到,ts 模板字符串中,类型推导确实是非贪婪的
【类型】函数(Function)
函数逆变(Contravariant)
关于逆变、协变等概念,可以参考 ‣
然后,关于逆变的应用,可以看下题(UnionToTuple)
这里还有另外一个 Trick:函数重载在 ts 中会 defer 推导,并且只展示最后一个重载函数的定义
函数重载(overload)
参考上面一题中的一个回答
‣
his is a feature of TS, mentioned somewhere in the documentation -- if it is necessary to output one type from overload, TS selects the last signature ((x: 2) => 0
) in the overload.
但是我没找到官方文档中有这样的说法,不过实际效果确实是 vscode 中鼠标 hover 的结果是最后一个,这里应该还涉及到 Typescript
LSP(Language Server Protocal)
的实现。待有空再研究吧【机制】Narrowing
触发条件
通常,Narrowing 是通过 js 的条件语句,ts 隐式推导变量类型,Narrowing 的途径包括:
typeof
关键字
- Equality 判断(即
===
,!==
等)
in
关键字
instanceof
关键字
- 赋值操作
- 控制流(Control Flow)if-else,switch-case 等。注意控制流不支持泛型 Narrowing
- type predicates:即定义
function isFish(): x is Fish
- JS
assert
:这个用的很少,而且感觉容易和测试库(jest、vitest 等)冲突
但是,我们也可以通过 trick,显式 Narrowing:
Narrowing失效 - 闭包、回调
某些场景下,TS 的 narrowing 可能会失效,但是你或许不应该认为这是 bug,而是通过其他途径(例如 Type Guard 断言函数或者在内部增加判断等)来避免
原因是 TS 作为静态解释器,闭包(Closure)和回调(Callback)等上下文场景无法确保执行 Narrowing 是可行的,详细可以看这篇blog ‣
Narrowing失效 - 泛型(Generics)
非常遗憾,TS 不支持对泛型通过 Control-Flow 进行 Narrowing(‣),因此,如果你希望函数实现以下的效果
只能够将 generic 替换为 Union:
【分析】
- TS 无法通过控制流(Control Flow)对 Generic 做 Narrowing
- TS 可以通过控制流对变量(即示例中的
params
)做 Narrowing
- 但是 Generic 可以通过函数传参时自动推导(个人认为这也算是一种 Narrowing,所以 TS 不支持 Control-Flow 的 Generic Narrowing 是否可以认为是一个 bug?)
【机制】类型推导 Type Inference
infer
总结一些 infer 的 tricks:
- 泛型使用 infer(似乎要求 TS 4.8 以上)
Best common type
根据官方提供的 demo,ts 的隐式类型(no explicit type)推导
关于TS隐式推导的规则,官网没有给出完整的说明,目测只能阅读源码才知道了……
不过可以通过几个 case,观察出端倪
- 对于 Promitive Type
- 如果对象是
let
,推导为string
- 如果对象指定为
const
,推导为“single”
- 对于数组 Array
- 如果不指定
as const
,会推导为通用类型(例如string
/number
/boolean
) - 如果指定
as const
,会推导为常量 - 不论是否指定
as const
,都无法推导获得父类(上方 Animal 的 case)
因此我们可以假设:ts 会根据对象是否可变(const or not),从而决定是否能尽量“向下”推导类型,如果对象可变,那就无法假设对象是字符串常量类型。
另外对于
class
类型,ts 无法推导出父类,如果想自动提取通用类型,目前看只能 object
可以自动推导, class
对于 ts 是无法自动获取类型的(感觉只能用 reflect-metadata)但是在实际应用中,感觉没必要这么麻烦, ROI 最高的还是显式定义了(例如
zoo: Animal[]
)官方文档(Indexed Access Types):‣
泛型数组自动获取常量类型
以一个体操题为例:
这里我们期望 result 对应的类型是
根据上述 array 部分的分析,其实同样可以应用
const
的逻辑到泛型中,例如:- 指定泛型
K
为const K
- 或者标识实例数组对象为
as const
- 另外,还有一种针对泛型的方法:
extends
Contextual Typing
全局上下文的类型获取,可以通过
declare var
或者 interface
指定,两者是等效的但是工程上,contextual typing 往往跟项目工程化有关(即 ts.config 配置)例如:
- lib、typeRoots 等目录指定上下文类型来源
- moduleDetection:该参数控制是否自动导出某个文件的类型到全局。说人话就是,如果该参数设置为 true,那你就不能在其他文件中定义相同名字的 type
【机制】条件类型(Conditional Types)
所谓条件类型(Conditional Types),即用到关键字
extends
配合三元运算符 ?:
来完成 ts 的类型推导,通常是结合 infer
来使用这里主要总结一些 tricks,基本用法不做过多解释
Trick 1: extends 左侧不能是 property 字段
例如下题,
T["type"]
用 extends 就不会生效Trick 2: Distributive现象([T] extends [any]的奇葩写法)
当 ts 中出现泛型 + 条件类型时,会触发 Distributive
关于原因,可以参考 ,这位老哥解释得很详细了
TLDR,有两个因素导致该现象:
- 泛型碰到条件类型,会触发 Distributive
- never 在联合类型中 distributive 时,被会忽略跳过
那么,怎么解决?Avoid Distributive:
Trick3: 提取 Union 类型中的某一个值
思路:通过 Union 条件判断函数参数 + infer 获取最后一个值:
【机制】映射(Mapped types)
映射类型的基础用法,大多时候是通过
in keyof
来剔除原 interface 中的 readonly
/ ?
等限制一个小 Trick:
keyof
遍历的元素可以联合(union),例如:该用法可用在下题中
数组映射
数组类型,在 ts 中似乎等同于(不确定)
{[number]: element}
和字符串类似,数组同样可以用
infer
取到首个元素,并递归遍历:映射类型(Mapped Types) + 索引访问(Indexed Access Type)
看如下示例(引自 ‣)
根据 copilot 的解释:
索引访问类型[Union1]
:
- 紧跟在映射类型之后的
[]
是索引访问类型的语法。
- 这里,它使用
Union1
作为索引来访问映射类型的结果。
- 这意味着它将从映射类型中选择所有
Union1
成员对应的类型,并将它们合并成一个联合类型。
- 如果
Union1
是'A' | 'B'
,那么索引访问的结果将是{ eventName: 'A'; featureName: string } | { eventName: 'B'; featureName: string }
。
通过 as
做重映射(Key Remapping)
看下面这题,实现 MyOmit:
上面的例子中,就结合了 Key Remapping + Conditional Types
另外有一点注意,
as
后不一定必须跟原映射的泛型,例如:这个写法也是语法正确的,你可以把上述 ts 语法想象成:
当然如果你这么写,就不符合 Omit 的逻辑要求了
但有些时候,我们在 Conditional Types 的判断位置上,需要用其他的泛型变量(之前遇到过,不过现在想要的时候找不到了😅)
映射 + 字符串模板(Template Literal Types)
其实字符串模板配合映射,有很 newbee 的用法,参考官方的示例:
就问你 n 不 nb。稍微再遐想一下,前端项目中,EventEmitter 的范式是很常见的,如果底层的 ts 工作能做好,自动将 event name 映射到 on/emit 等类型,一方面能规避低级失误,另外也能提效开发(不用再去找 payload 的类型了)OKR get daze 😏
LSP(Language Server Protocal)
- 函数重载只展示最后一个
- 联合 interface 不会自动推导,需要再自行重映射,如下
TS图灵完备性(Turing completeness)
‣
- 作者:Tony
- 链接:https://wangqiwei.life/article/ts-exercise
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。