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

【类型】基础类型

下面罗列一些基本类型的 trick:

number/string 比较大小

P.S.
这里 string 指的也是 `${number}` 类型的 string 比较,不考虑 alphabet 拼写顺序之类的字符比较
通常比较 number,都是将 number 转换为对应大小的数组,然后比较数组之间的大小
上面利用了 infer 的 trick,但是注意它可能具有以下问题:
  1. 只能判断 T > U,而非 T ≥ U,等于的 case 会返回 false(不过针对这一点将 U/T 反过来就好)
  1. 如果number 过大,会占用较大内存,如下
notion image
因此最好的思路是转成 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 点实现:
  1. T extends string
  1. 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)应当如下
💡
  1. 获取当前需要匹配的位置(placeholder),即 ${} 内部的变量,假设是 P
  1. 从当前位置,左往右遍历字符串,每次尝试加入 1 个 char 作为 P 的值,如果不匹配,就继续拼接,直到第一次匹配或者走到字符串末尾
  1. 匹配 P 之后,继续匹配下一个模板字符串中的变量,更新变量 P 和当前位置,并重复 1,2过程
  1. 如果走到字符串结尾,完成匹配,直接返回结果(因此可以认为 TS 和正则不同,默认是非贪婪模式)
  1. 如果走到字符串结尾,未完成匹配,则
    1. 判断有没有 P 已经占用了空字符,如果没有,设置当前 P 为空字符(前提是 P 为 string、any 等空字符的父类类型)
    2. 空字符已占用,尝试弹出上一个匹配的 P,在之前匹配 P 的值基础上,继续拼接后续字符,找到更长的子串作为 P 的值
    3. 如果找不到,持续弹出,直到栈为空并且走到字符串结尾
至于 ${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 关键字
  • in 关键字
  • instanceof 关键字
  • 赋值操作
  • 控制流(Control Flow)if-else,switch-case 等。注意控制流不支持泛型 Narrowing
  • 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 的逻辑到泛型中,例如:
  • 指定泛型 Kconst 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,有两个因素导致该现象:
  1. 泛型碰到条件类型,会触发 Distributive
  1. 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)

 
MacOS ShortCut Issue - Cmd + HSayings and Interpretations