TypeScript 类型体操之基本原理

128 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 8 天

体操基本原理

// 当我们想写
if( A <= B ) true else false 

// 那么可以这样写
type A = 1
type B = 12
type Result = A extends B ? true : false
// 如果 if else 复杂一点 比如说有两个条件呢?
if( A <= B ) and ( C <= D ) ...

// 那么可以这样写
type A = 1
type B = 1 | 2
type C = 3
type D = 3 | 4
type Result = A extends B
  ? C extends D
    ? "true, true"
    : "true, false"
  : C extends D
  ? "false, true"
  : "false, false"

空元组

数组就是长度无限的

元组就是长度有限制

// 注意:这里的数组是类型,不是 JS 的值,一般把这种叫做 元组
type A = []
// 判断一个元组是不是空元组
type IsEmptyArray<Arr extends unknown[]> = 
  Arr['length'] extends 0 ? true : false
type Result = IsEmptyArray<A>
//   ^-- true

非空元组

type A = [1]
type NotEmpty<Arr extends unknown[]> = 
     Arr extends [...infer X, infer Last] ? true : false
type Result = NotEmpty<A>
//   ^-- true

Arr extends [...infer X, infer Last] ? true : false

也可以写成

Arr extends [...unknown[], unknown] ? true : false

那为什么要加 infer ?

因为有的时候我们可能不想知道它到底是什么,不想写类型。

就用变量表示,可以吗?

Arr extends [...X, Y] ? true : false 但是这样写就会报错

这个时候如果加上 infer

Arr extends [...infer X, infer Y] ? true : false

那 TS 就知道我写的是类型,只是不想告诉 TS 这个是什么。这里的 X 其实引用了 X = unknown[] 就是给 unknown[] 数组取一个名字为 X,这个 X 可以为空,Y 其实也是给 unknown 取一个名字。

递归

type A = ['ji', 'ni', 'td', 'mw']
type Reverse<Arr extends unknown[]> = 
  Arr extends [...infer Rest, infer Last]
    ? [Last, ...Reverse<Rest>]
    : Arr
type Result = Reverse<A>
// Result is ['mw', 'td', 'ni', 'ji']

模式匹配 + infer"引用"

type Tuple = ['ji', 'ni', 'td', 'mw'] // 这里面的都是类型,不是元素
type Result1 = Tuple extends [infer First, ...infer Rest]
// 这个可以写成 type Result1 = Tuple extends [infer First, ...string[]]
  ? First : never
// Result1 is 'ji'

type Result2 = Tuple extends [infer First, ...infer Rest]
// 可以写成  type Result2 = Tuple extends ['ji', ...infer Rest]
  ? Rest : never
// Result2 is ['ni', 'td', 'mw']

模式匹配:我用我这个模式去匹配你那个模式,如果两个匹配上了,那它就自动赋值。

如果不写 infer, 就会提示 cannot find name 'First', 所以可以认为 infer 其实就是 var,也可以认为 infer 其实就是引用,这个 First 它引用的是当前元素的这个类型。

获取元组的长度

type A = ['ji', 'ni', 'td', 'mw']
type R = A['length'] // 没有报错
// 因为这是 JS 的特性

元组的基本体操

// 把一个元组变的长度更长
type A = [1]  // 有的时候 A 里面 类型很长/类型很复杂
type B = [...A, 2]  // 复制的时候就会觉得好长不想复制,于是就用 ... 把它弄下来
type B = [1, 2]
type C = [3, 4]
type D = [...B, ...C]
type D = [1, 2, 3, 4]
// 注意 1, 2, 3, 4 都是类型, 不是值
type Last<T> = T extends [...items: unknown[], last: infer X]
       ? X
       : never
type E = Last<D> // 获取的是最后一项
//   ^--- E = 4 注意: 这里的 4 是类型,不是值
// 一个错误的写法
type Last<T extends unknown[]> = T[T['length'] - 1] // TS 并没有提供 - 1 的操作
// 如果想获取除了最后一项的其他项
type D = [1, 2, 3, 4]
// 注意 1, 2, 3, 4 都是类型, 不是值
type NoLast<T> = T extends [...infer X, unknown]
       ? X
       : never
type E = NoLast<D> // 获取的是最后一项
//   ^--- E = [1, 2, 3] 注意: 这里的 1, 2, 3 是类型,不是值

字符串的基本体操

内置的 intrinsic

type A = 'hone'
type B = Capitalize<A>  // 这个 Capitalize 是 TS 已经帮我们写好的
//   ^-- type B = 'Hone'
type C = 'ji' | 'ni' | 'td' | 'mw'
type X = Capitalize<C>
//   ^-- type X = 'Ji' | 'Ni' | 'Td' | 'Mw'

Screen Shot 2022-10-16 at 6.39.43 PM.png intrinsic 的意思是这玩意我已经内置了,你不用管它是怎么实现的(内在的、固有的)。

哪些是内置的 intrinsic:

  • Uppercase 全变成大写
  • Lowercase 全部变小写
  • Capitalize 首字母大写
  • Uncapitalize 首字母小写

使用以上内置的 intrinsic,类型的顺序可能会变,因为联合类型它就是没有顺序的,元组是有顺序的。

模版字符串

// 注意: 以下写法都是类型,不是值
type A = 'ji'
type B = 'ni'
type C = 'td'
type D = 'mw'
type X = `${A} ${B} ${C} ${D}`
//   ^-- type X = 'ji ni td mw'

获取字符串的第一个

// 获取第一个
type A = 'ji ni td mw'

// 这个 ${infer F} 永远是第一个, ${string} 永远是除第一个外
type First<T extends string> = T extends `${infer F}${string}` ? F : never
type Result = First<A>
//   ^-- 'j'

// 如果 ${infer F}${string} 写成 ${string}${string} 就不知道如何分配了
// 有可能匹配到   '' ‘ji ni td mw’
// 也有可能匹配到 ‘ji ni td m’  ‘w‘

获取字符串的最后一个

// 如何获取字符串的最后一个
// 我们可以获取元组的最后一项
type LastOfTuple<T extends unknown[]> = 
  T extends [...infer _, infer L] ? L : never

// 字符串可以转为元组  
type StringToTuple<S extends string> = 
  S extends `${infer F} ${infer R}`  
    ? [F, ...StringToTuple<R>]
    : []
    
// 我们可以获取字符串的最后一项
type LastOfString<S extends string> = 
  LastOfTuple<StringToTuple<S>>
type R = LastOfTuple<"ji ni td mw">

infer 的文档在哪里

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type

Here, we used the infer keyword to declaratively introduce a new generic type variable named Item instead of specifying how to retrieve the element type of T within the true branch. This frees us from having to think about how to dig through and probing apart the structure of the types we’re interested in.

大概意思: 使用 infer 关键字来显式的声明一个新的泛型变量用来代替之前的类型。你可以直接写一个 infer Item 你就不用管这个 Itemnumber 还是 string 还是 unknown , 你直接用一个变量来代替它,不管它是什么,我就原封不动的返回

infer 文档地址: 链接🔗

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

type X = Flatten<string[]> // 获取到 string[] 里面 string

type Y = Flatten<Array<number>>  // 获取到 Array 里面的 number
// Array 里面有什么东西,它就可以被 Item 引用

如何自己搜到 infer 的定义

直接搜?

每个结果点进去 cmd/ctrl + f 全局搜索 发现并没有找到与 infer 相关的。

我们要搜 Conditional Types,点进去然后搜 infer

根据文档的排版说明: 暗示 infer 通常是出现在 extends 里面的。

TS 递归的层数限制


当前场景根据测试最多递归 48层。

Screen Shot 2022-10-17 at 4.22.51 PM.png 把每一个 key 都加上 Readonly。

interface SomeObject {
  a: {
    b: {
      c: number
    }
  }
}

const obj: Readonly<SomeObject> = { a: { b: { c: 2 } } }

// 所以对 a 进行赋值就会报错

字符串转元组、转联合

// 字符串转元组  
type StringToTuple<S extends string> = 
  S extends `${infer First} ${infer Rest}`  
    ? [First, ...StringToTuple<Rest>]
    : []
type Result = StringToTuple<'jinitdmw'>
// type Result = ['j', 'i', 'n', 'i', 't', 'd', 'm', 'w']
// 字符串转联合
// never 和任何类型联合起来是不影响结果的
type StringToUnion<S extends string> =
  S extends `${infer First}${infer Rest}`
  ? First | StringToUnion<Rest>
  : never
type Result = StringToUnion<'jinitdmw'>
// type Result = 'j' | 'i' | 'n' | 't' | 'd' | 'm' | 'w'
// 注意,联合类型自动去重了