# Type-Challenges

# 介绍

在学习完TypeScript一些基础知识后,我们已经可以熟练使用一些基本类型定义了,但对于TypeScript的高级用法却依旧无法入门,为了更有趣的学习TypeScript高级用法,我们选择Type-Challenges (opens new window)类型挑战来作为我们学习的目标。

Type-Challenges中,我们可以从简单中等困难以及地狱难度,循序渐进的学习TypeScript高级技巧。

如果你需要选择其他的方向来深入学习高级技巧,这里也有一些推荐的开源项目:

在之后的挑战中,我们会尽力对每道题进行必要的讲解,力争在进行Type-Challenges类型挑战时收益最大化。

# 核心知识点

# 加号和减号

TIP

加号和减号的用法类似,不赘述,只介绍减号。

在一些内置工具中,可能会出现+或者-这些符号,例如:

type Required<T> = {
  [P in keyof T]-?: T[P]
}
type Person = {
  name: string;
  age?: number;
}

// 结果:{ name: string; age: number; }
type result = Required<Person>

观察结果我们可以知道,-?是去掉类型中属性后面的?,整个Required的实际效果是去掉T类型中所有属性键后面的?,让所有属性变成必填的。

# keyof 和 in

keyofin经常会连在一起使用,当它们连在一起使用时,通常表示一个迭代的过程。

# keyof

TS中,keyof T这段代码表示获取T类型中所有属性键,这些属性键组合成一个联合类型,例如:

type Person = {
  name: string;
  age: number;
}
// 'name' | 'age'
type result = keyof Person

TS中的keyof T,它有点类似JavaScript中的Object.keys(),它们的共同点都是获取属性键的集合,只不过keyof T得到的结果是一个联合类型,而Object.keys()得到的是一个数组。

# in

in操作符的右侧通常跟一个联合类型,可以使用in来迭代这个联合类型,如下:

// 仅演示使用
in 'name' | 'age' | 'sex'
'name' // 第一次迭代结果
'age'  // 第二次迭代结果
'sex'  // 第三次迭代结果

TS中的in操作符原理,跟JavaScript中的for in遍历有点类似。

根据keyofin的特点,我们可以撰写一些辅助工具,这里以Readonly为例。

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}
type Person = {
  name: string;
  age: number;
}
// 结果:{ readony name: string; readonly age: number; }
type result = Readonly<Person>

代码详解:

  • [P in keyof T]:这段代码表示遍历T中的每一个属性键,每次遍历时属性键取名为P,这和JavaScript中的for in非常类似:
// ts中的迭代
P in keyof T

// JavaScript中的迭代
for (let key in obj) 

# typeof

TS中的typeof,可以用来获取一个JavaScript变量的类型,通常同于获取一个对象或者一个函数的类型,如下:

const add = (a: number, b: number): number => {
  return a + b
}
const obj = {
  name: 'AAA',
  age: 23
}

// 结果:(a: number, b:number) => number
type t1  = typeof add
// 结果:{ name: string; age: number; }
type t2 = typeof obj

# never

never类型表示永远不会有值的一种类型。

如果一个函数抛出了一个错误,那么这个函数就可以用never来表示其返回值,如下:

function handlerError(message: string): never {
  throw new Error(message)
}

关于never的另外一个知识点是:如果一个联合类型中存在never,那么实际的联合类型并不会包含never,如下:

// 定义
type test = 'name' | 'age' | never

// 实际
type test = 'name' | 'age'

# extends

extends关键词,一般有两种用法:类型约束和条件类型。

# 类型约束

类型约束经常和泛型一起使用:

// 类型约束
U extends keyof T

keyof T是一个整体,它表示一个联合类型。U extends Union这一整段表示U的类型被收缩在一个联合类型的范围内。

这样做的实际表现为:第二个参数传递的字符串只能是T键名中的一个,传递不存在的键名会报错。

# 条件类型

常见的条件类型表现形式如下:

T extends U ? 'Y' : 'N'

我们发现条件类型有点像JavaScript中的三元表达式,事实上它们的工作原理是类似的,例如:

type res1 = true extends boolean ? true : false // true
type res2 = 'name' extends 'name'|'age' ? true : false // true
type res3 = [1, 2, 3] extends { length: number; } ? true : false // true
type res4 = [1, 2, 3] extends Array<number> ? true : false // true

在条件类型中,有一个特别需要注意的东西就是:分布式条件类型,如下:

// 内置工具:交集
type Extract<T, U> = T extends U ? T : never;

type type1 = 'name'|'age'
type type2 = 'name'|'address'|'sex'

// 结果:'name'
type test = Extract<type1, type2>

// 推理步骤
'name'|'age' extends 'name'|'address'|'sex' ? 'name'|'age' : never
=> ('name' extends 'name'|'address'|'sex' ? 'name' : never) |
   ('age' extends 'name'|'address'|'sex' ? 'age' : never)
=> 'name' | never
=> 'name'

代码详解:

  • T extends U ? T : never:因为T是一个联合类型,所以这里适用于分布式条件类型的概念。根据其概念,在实际的过程中会把T类型中的每一个子类型进行迭代,如下:
// 第一次迭代:
'name' extends 'name'|'address'|'sex' ? 'name' : never
// 第二次迭代:
'age' extends 'name'|'address'|'sex' ? 'age' : never
  • 在迭代完成之后,会把每次迭代的结果组合成一个新的联合类型(剔除never),如下:
type result = 'name' | never => 'name'

# infer

infer关键词的作用是延时推导,它会在类型未推导时进行占位,等到真正推导成功后,它能准确的返回正确的类型。

为了更好的理解infer关键词的用法,我们使用ReturnType这个例子来说明,ReturnType是一个用来获取函数返回类型的工具。

type ReturnType<T> = T extends (...args: any) => infer R ? R : never

const add = (a: number, b: number): number => {
  return a + b
}
// 结果: number
type result = ReturnType<typeof add>

代码详解:

  • T extends (...args: any) => infer R:如果不看infer R,这段代码实际表示:T是不是一个函数类型。
  • (...args: any) => infer R:这段代码实际表示一个函数类型,其中把它的参数使用args来表示,把它的返回类型用R来进行占位。 如果T满足是一个函数类型,那么我们返回其函数的返回类型,也就是R;如果不是一个函数类型,就返回never

# & 符号

TS中有两种类型值得我们关注:联合类型交叉类型

联合类型一般适用于基本类型的"合并",它使用|符号进行连接,如下:

type result = 'name' | 1 | true | null

而交叉类型则适用于对象的"合并",它使用&符号进行连接,如下:

type result = T & U

T & U表示一个新的类型,其中这个类型包含TU中所有的键,这和JavaScript中的Object.assign()函数的作用非常类似。

根据交叉类型的概念,我们可以手动封装一个对象的merge函数,如下:

const obj1 = { name: 'AAA' }
const obj2 = { age: 23 }
function merge<T, U>(to: T, from: U): T & U {
  for (let key in from) {
    ;(to as T & U)[key] = from[key] as any
  }
  return to as T & U
}

// 结果:{ name:'AAA'; age: 23; }
const resutl = merge(obj1, obj2)

# 初级

# Partial(可填)和Required(必填)

# 用法

PartialRequired一个是让所有类型可填、另外一个是让所有类型必填,用法如下:

type Person = {
  name: string;
  age?: number;
}

// 结果: { name?: string; age?: number; }
type PartialResult = MyPartial<Person>

// 结果: { name: string; age: number; }
type RequiredResult = MyRequired<Person> 

# 实现方式

type MyPartial<T> = {
  [P in keyof T]?: T[P]
}
type MyRequired<T> = {
  [P in keyof T]-?: T[P]
}

# Readonly(只读)和Mutable(可改)

# 用法

ReadonlyMutable一个是让所有属性变为只读,另外一个是让所有属性变为可改的(移除readonly关键词),其用法为:

type Person = {
  readonly name: string;
  age: number;
}

// 结果:{ readonly name: string; readonly age: number; }
type ReadonlyResult = Readonly<Person>

// 结果:{ name: string; age: number; }
type MutableResult = Mutable<Person>

# 实现方式

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}
type MyMutable<T> = {
  -readonly [P in keyof T]: T[P]
}

代码解读:

  • -readonly:表示把readonly关键词去掉,去掉之后此字段变为可改的.

# Pick(选取)

# 用法

Pick表示从一个类型中选取指定的几个字段组合成一个新的类型,用法如下:

type Person = {
  name: string;
  age: number;
  address: string;
  sex: number;
}
// 结果: { name: string; address: string; }
type PickResult = Pick<Person, 'name' | 'address'>

# 实现方式

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

代码详解:

  • K extends keyof T:表示K只能是keyof T的子类型,如果我们在使用Pick的时候传递了不存在于T的字段,会报错:
// 报错:phone无法分配给keyof T
type result = MyPick<Person, 'name' | 'phone'>

# Exclude(排除)

# 用法

Exclude是排除的意思,它从T类型中排除属于U类型的子集,可以理解成取T对于U的差集,用法如下:

// 结果:'name'|'age'
type ExcludeResult = Exclude<'name'|'age'|'sex', 'sex'|'address'>

# 实现方式

type MyExclude<T, U> = T extends U ? never : T
  • T extends U:这段代码会从T的子类型开始分发,例如:
T extends U 
=> 'name'|'age'|'sex' extends 'sex'|'address'
=> (
  'name' extends 'sex'|'address' ? never : 'name' |
  'age' extends 'sex'|'address' ? never : 'age' |
  'sex' extends 'sex'|'address' ? never : 'sex'
)
=> 'name'|'age'

# Omit(移除)

# 用法

Omit是移除的意思,它用来在T类型中移除指定的字段,用法如下:

type Person = {
  name?: string;
  age: number;
  address: string;
}

// 结果:{ name?: string; age: number; }
type OmitResult = Omit<Person, 'address'>

# 实现方式

Omit可以借助在上面已经实现过的PickExclude配合来实现,如下:

// Omit实现
type MyOmit<T, K> = MyPick<T, MyExclude<keyof T, K>>

代码详解:

  • 使用MyExclude<keyof T, K>,可以从T中移除指定的字段,移除后得到一个新的联合类型:'name'|'age'
  • 使用MyPick<T, 'name'|'age'>,可以从T中选取这两个字段,组合成一个新的类型。

# Record(构造)

# 用法

Record<K, T>用来将K的每一个键(k)指定为T类型,这样由多个k/T组合成了一个新的类型,用法如下:

type keys = 'Cat'|'Dot'
type Animal = {
  name: string;
  age: number;
}
type Expected = {
  Cat: {
    name: string;
    age: number;
  };
  Dog: {
    name: string;
    age: number;
  }
}

// 结果:Expected
type RecordResult = Record<keys, Animal>

# 实现方式

type MyRecord<k extends keyof any, T> = {
  [P in K]: T
}

代码详解:

  • k extends keyof any:此代码表示Kkeyof any任意类型其所有键的子类型,例如:
// K为 'Dog'|'cat'
type UnionKeys = 'Dog' | 'Cat'

// K为'name'|'age'
type Person = {
  name: string;
  age: number;
}
type TypeKeys = keyof Person

# Extract(交集)

# 用法

Extract<T, U>用来取联合类型TU的交集,用法如下:

type Person = {
  name: string;
  age: number;
  address: string;
}

// 结果:{ age:number;address:string; }
type ExtractResult = Extract<keyof Person, 'age'|'address'|'sex'>

# 实现方式

type MyExtract<T, U> = T extends U ? T : never

代码详解:

  • T extends U:此代码会自动将T的子类型进行分发,例如:
T extends U
=> 'name'|'age'|'address' extends 'age'|'address'|'sex' ? T : never
=> (
  'name' extends 'age'|'address'|'sex' ? 'name' : never |
  'age' extends 'age'|'address'|'sex' ? 'age' : never |
  'address' extends 'age'|'address'|'address' ? 'age' : never
)
=> 'age'|'address'

# TupleToObject(元组转对象)

# 用法

TupleToObject<T>是用来把一个元组转换成一个key/value相同的对象,例如:

type tuple = ['msg', 'name'] as const
// 结果:{ msg: 'msg'; name: 'name'; }
type result = TupleToObject<typeof tuple>

# 实现方式

type TupleToObject<T extends readonly any[]> = {
  [P in T[number]]: P
}

代码详解:

  • as const:常用来进行常量断言,在此处表示将['msg','name']推导常量元组,表示其不能新增、删除、修改元素,可以使用as readonly来辅助理解。
  • T[number]:表示返回所有数字型索引的元素,形成的一个联合类型,例如:'msg'|'name'

# First(数组第一个元素)

# 用法

First<T>用来返回数组的第一个元素,用法如下:

// 结果:3
type result1 = First<[3, 2, 1]>
// 结果:never
type result2 = First<[]>

# 实现方式

type First<T extends any[]> = T extends [] ? never : T[0]

代码详解:

  • T extends []:用来判断T是否是一个空数组。
  • T[0]:根据下标取数组第一个元素。

# Length(元组的长度)

# 用法

Length<T>用来获取一个数组(包括类数组)的长度,用法如下:

// 结果:3
type result1 = Length<[1, 2, 3]>
// 结果:10
type result2 = Length<{ 5: '5', length: 10 }>

# 实现方式

type Length<T extends any> = T extends { length: number; } ? T['length'] : never

代码详解:

  • T extends { length: number; }:判断T是否是{ length: number; }的子类型,如果是则代表T为数组或者类数组。
  • T['length']:取T对象的length属性的值(注意,在TypeScript中不能使用T.length来取值,而应该使用T['length'])。

# ReturnType(函数返回类型)

# 用法

ReturnType<T>是用来获取一个函数的返回类型的,例如:

function getRandom (): number {
  return Math.random()
}
// 结果:number
type result = ReturnType<typeof getRandom>

# 实现方式

type ReturnType<T> = T extends (...args: any) => infer R ? R : never

代码详解:

  • T extends (...args: any) => infer R:判断T类型是否是一个函数的子类型,既T是不是一个函数。
  • infer R:表示待推导的函数返回类型为R,后续可以在表达式中使用R来代替真正的返回类型,知识点infer

# PromiseType(promise包裹类型)

# 用法

PromiseType是用来获取Promise包裹类型的,例如:

function getInfo (): Promise<string|number> {
  return Promise.resolve(1)
}
// 结果:() => Promise<string|number>
type funcType = typeof getInfo
// 结果:Promise<string|number>
type returnResult = ReturnType<funcType>
// 结果:string|number
type PromiseResult = PromiseType<returnResult>

# 实现方式

type PromiseType<T> = T extends Promise<infer R> ? R : never

代码详解:

  • T extends Promise<infer R>:判断T是否是Promise<infer R>的子类型,也就是说T必须满足Promise<any>的形式。

# If(判断)

# 用法

If<C, T, F>用来表示根据C的值来返回T或者F,如果Ctrue,则返回T;如果Cfalse,则返回F,例如:

// 结果:'a'
type result1 = If<true, 'a', 'b'>
// 结果:‘b’
type result2 = If<false, 'a', 'b'>

根据上案例,我们可以直观的发现If<C, T, F>的作用有点类似JavaScript中的三元表达式:C ? T : F

# 实现方式

type If<C extends boolean, T, F> = C extends true ? T : F

代码详解:

  • C extends boolean:表示Cboolean类型的子类型,既C只能为true或者false,传递其它值报错。
  • C extends true:如果用JavaScript来表示的话,相当于C===true.

# Concat(数组concat方法)

# 用法

Concat<T, U>用来实现讲两个数组合并起来,类似实现数组的concat方法,使用方式如下:

// 结果:[1, 2, 3, 4]
type result = Concat<[1, 2], [3, 4]>

# 实现方式

type Concat<T extends any[], U extends any[]> = [...T, ...U]

代码详解:

  • T extends any[]:用来限制T是一个数字,如果传递非数组会报错,U也是一样的道理。
  • [...T, ...U]:我们可以理解成JavaScript的扩展运算符...

# Includes(数组includes方法)

# 用法

Includes<T, U>用来判断U是否在在数组T中,类似实现数组的includes方法,用法如下:

// 结果:true
type result1 = Includes<[1, 2, 3], 1>
// 结果:false
type result2 = Includes<[1, 2, 3], '1'>

# 实现方式

type Includes<T extends any [], U> = U extends T[number] ? true : false

代码详解:

  • T[number]:它返回数组中所有数字类型键对应的值,将这些值构造成一个联合类型,例如:1 | 2 | 3
  • U extends T[number]:判断U是否是某个联合类型的子类型,例如:1 extends 1 | 2 | 3

# 中级

# Readony(按需Readonly)

# 用法

不同于初级实现中的Readonly,在中级实现的Readonly中,如果我们传递了指定的字段,那么Readonly会表现为按需实现readonly,用法如下。

interface Todo {
  title: string;
  desc?: string;
  completed: boolean;
}
interface Expected1 {
  readonly title: string;
  readonly desc?: string;
  readonly completed: boolean;
}
interface Expected2 {
  title: string;
  readonly desc?: string;
  readonly completed: boolean;
}

// 结果:Expected1
type ReadonlyResult1 = Readonly<Todo>
// 结果:Expected2
type ReadonlyResult2 = Readonly<Todo, 'desc'|'completed'>

// 测试:
const obj: ReadonlyResult2 = {
  title: 'AAA',
  desc: '23',
  completed: true
}
obj.title = 'aaa'
obj.desc = '32' // error
obj.completed = false // error

# 使用方式

type Readonly<T, K extends keyof T = any> = T & {
  readonly [P in keyof T as P extends K ? P : never]: T[P]
}

代码详解:

  • K extends keyof T = any:如要传递了K,那么只能是T中已经存在的属性,不存在则报错;如果不传递,则默认值为any,意味着全部属性都添加readonly
  • asT as U表示对于T进行进一步的加工/判断,在此处具体表现为:我们只对指定字段进行迭代并添加readonly关键词。
  • T & U:在本例中表示将TU中的字段结合起来,如果没有&,那么就丢失一些属性,例如title

# DeepReadonly(深度Readonly)

# 用法

DeepReadonly用来将一个嵌套对象类型中所有字段全部添加readonly关键词,例如:

// 类型:
type X = {
  b: string
  c: {
    d: boolean
    e: undefined,
    f: null
  }
}
// 结果:
type Y = {
  readonly b: string
  readonly c: {
    readonly d: boolean
    readonly e: undefined,
    readonly f: null
  }
}

# 实现方式

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends { [key: string]: any } ? DeepReadonly<T[P]> : T[P]
}

代码详解:

  • T[P] extends { [key: string]: any }:这段表示T[P]是否是一个包含索引签名的字段,如果包含我们认为它是一个嵌套对象,就可以递归调用DeepReadonly

# TupleToUnion(元组转联合类型)

# 用法

TupleToUnion是用来将一个元组转换成联合类型的,其用法如下:

// 结果:'1' | '2' | '3'
type result = TupleToUnion<['1', '2', '3']>

# 实现方式

// way1: T[number]
type TupleToUnion<T extends readonly any[]> = T[number]

// way2: 递归
type TupleToUnion<T extends readonly any[]> = 
  T extends [infer R, ...infer args]
    ? R | TupleToUnion<args>
    : never

代码详解:

  • T[number]:它会自动迭代元组的数字型索引,然后将所以元素组合成一个联合类型。
  • R | TupleToUnion<args>:R表示每一次迭代中的第一个元素,它的迭代过程可以用下面伪代码表示:
// 第一次迭代
const R = '1'
const args = ['2', '3']
const result = '1' | TupleToUnion<args>

// 第二次迭代
const R = '2'
const args = ['3']
const result = '1' | '2' | TupleToUnion<args>

// 第三次迭代
const R = '3'
const args = ['']
const result = '1' | '2' | '3'

# Last(数组最后一个元素)

# 用法

Last是用来获取数组中最后一个元素的,它和我们之前已经实现的First思路很相似。

// 结果:3
type result = Last<[1, 2, 3]>

# 实现方式

Last的实现方式很巧妙,因为它既可以在索引上做文章来实现,也可以用占位的思想来实现。

// way1:索引思想
type Last<T extends any[]> = [any, ...T][T['length']]

// way2: 后占位思想
type Last<T extends any[]> = T extends [...infer R, infer L] ? L : never

代码详解:

  • [any, ...T]:此代码表示我们构建了一个新数组,并添加了一个新元素到第一个位置,然后把原数组T中的元素依次扩展到新数组中,可以用以下伪代码表示:
// 原数组
const T = [1, 2, 3]

// 新数组
const arr = [any, 1, 2, 3]

// 结果: 3
const result = arr[T['length']]
  • T['length']:这里我们获取到的是原始T数组的长度,例如[1, 2, 3],长度值为3。而在新数组中,索引为3的位置正好是最后一个元素的索引,通过这种方式就能达到我们的目的。
  • T extends [...infer R, infer L]:这段代码表示,我们将原数组中最后一个元素使用L进行占位,而其它元素我们用一个R数组表示。这样,如果数组满足这种格式,就能正确返回最后一个元素的值。

# Pop和Push

继续沿用以上处理索引思想和占位的思想,我们能快速实现数组pop方法和push方法。

# 用法

// Pop结果:[1, 2]
type popResult = Pop<[1, 2, 3]>

// Push结果:[1, 2, 3, 4]
type pushResult = Push<[1, 2, 3], 4>

# 实现方式

// Pop实现
type Pop<T extends any[]> = T extends [...infer R, infer L] ? R : never

// Push实现
type Push<T extends any[], K> = [...T, K]

# Shift和Unshift

poppush方法相似的另外一对方法叫shiftunshift,它们的实现思路是一样的。

# 用法

// Shift结果:[2, 3]
type shiftResult = Shift<[1, 2, 3]>

// Unshift结果:[0, 1, 2, 3]
type unshiftResult = Unshift<[1, 2, 3], 0>

# 使用方式

// Shift实现
type Shift<T extends any[]> = T extends [infer F, ...infer R] ? R : never

// Unshift实现
type Unshift<T extends any[], K> = [K, ...T]

# PromiseAll返回类型

# 用法

PromiseAll是用来取Promise.all()函数所有返回的类型,其用法如下

// 结果:Promise<[number, number, number]>
type result = typeof PromiseAll([1, 2, Promise.resolve(3)])

# 实现方式

与之前的例子不同,PromiseAll我们声明的是一个function而不是type

type PromiseAllType<T> = Promise<{
  [P in keyof T]: T[P] extends Promise<infer R> ? R : T[P]
}>
declare function PromiseAll<T extends any[]>(values: readonly [...T]): PromiseAllType<T>

代码详解:

  • 因为Promise.all()函数接受的是一个数组,因此泛型T限制为一个any[]类型的数组。
  • PromiseAllType的实现思路有点像之前的PromiseType,只不过这里多了一层Promise的包裹,因为Promise.all()的返回类型也是一个Promise

# Trim、TrimLeft以及TrimRight

# 用法

TrimTrimLeft以及TrimRight这几个工具比较好理解,它们都是用来移除字符串中的空白符的。

const t1 = TrimLeft<' str'>  // 'str'
const t2 = Trim<' str '>     // 'str'
const t3 = TrimRight<'str '> // 'str'

# 实现方式

type Space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? TrimLeft<R> : S
type Trim<S extends string> = S extends (`${Space}${infer R}` | `${infer R}${Space}`) ? Trim<R> : S
type TrimRight<S extends string> = S extends `${infer R}${Space}` ? TrimRight<R> : S

代码详解:

  • TrimLeftTrimRight的实现思路是相同的,区别在于空白符的占位出现在左侧还是右侧。
  • Trim的实现就是把TrimLeftTrimRight所做的事情结合起来。

# Capitalize(首字母大写)和Uncapatilize(首字母小写)

# 用法

Capitalize是用来将一个字符串的首字母变成大写的,而Uncapatilize所做的事情跟它相反,其用法如下:

type t1 = Capitalize<'hello'>   // 'Hello'
type t2 = Uncapatilize<'Hello'> // 'hello'

# 实现方式

type Capatilize<S extends string> = S extends `${infer char}${infer L}` ? `${Uppercase<char>}${L}` : S
type Uncapatilize<S extends string> = S extends `${infer char}${infer L}` ? `${Lowercase<char>}${L}` : S

代码详解:

  • 无论是Capatilize还是Uncapatilize,它们都依赖内置的工具函数Uppercase或者Lowercase。对于Capatilize而言,我们只需要把首字母隔离出来,然后调用Uppercase即可。对于Uncapatilize而言,我们把首字母调用Lowercase即可。

# Replace和ReplaceAll

# 用法

Replace是用来将字符串中第一次出现的某段内容,使用指定的字符串进行替换,而和ReplaceAll是全部替换,其用法如下:

type t1 = Replace<'foobarbar', 'bar', 'foo'>    // 'foofoobar'
type t2 = ReplaceAll<'foobarbar', 'bar', 'foo'> // 'foofoofoo'

# 实现方式

type Replace<
  S extends string,
  from extends string,
  to extends string
> = S extends `${infer L}${from}${infer R}`
      ? from extends ''
        ? S
        : `${L}${to}${R}`
      : S

type ReplaceAll<
  S extends string,
  from extends string,
  to extends string
> = S extends `${infer L}${from}${infer R}`
      ? from extends ''
        ? S
        : ReplaceAll<`${L}${to}${R}`, from, to>
      : S

# AppendArgument(追加参数)

# 用法

AppendArgument是用来向一个函数追加一个参数的,其用法如下:

//  结果:(a: number, b: number) => number
type result = AppendArgument<(a: number) => number, number>

# 实现方式

type AppendArgument<Fn, A> = Fn extends (...args: infer R) => infer T ? (...args: [...R, A]) => T : never

代码详解:

  • 我们首先利用infer关键词得到了Fn函数的参数类型以及返回类型,然后把新的参数添加到参数列表,并原样返回其函数类型。

# LengthOfString(字符串的长度)

# 用法

LengthOfString是用来计算一个字符串长度的,其用法如下:

type result = LengthOfString<'Hello'> // 5

# 实现方式

type LengthOfString<
  S extends string,
  T extends string[] = []
> = S extends `${infer Char}${infer R}`
      ? LengthOfString<R, [...T, Char]>
      : T['length']

代码详解:

  • 我们通过一个泛型的辅助数组来帮我们计算字符串的长度,在第一次符合条件时,我们将其第一个字符添加到数组中,在后续的递归过程中,如果不符合条件,直接返回T['length'],这个过程可以用如下代码表示:
// 第一次递归
const T = ['H'], S = 'hello', R = 'ello'
// 第二次递归
const T = ['H','e'], S = 'ello', R = 'llo'
// 第三次递归
const T = ['H','e','l'], S = 'llo', R = 'lo'
// 第四次递归
const T = ['H','e','l','l'], S = 'lo', R = 'o'
// 第五次递归
const T = ['H','e','l','l', 'o'], S = 'o', R = ''

# Flatten(数组降维)

# 用法

Flatten是用来将多维数组进行降维的,其用法如下:

// 结果:[1, 2, 3]
type result = Flatten<[1, 2, [3]]>

# 实现方式

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

代码详解:Flatten数组降维的主要思路是,遍历数组中的每一个元素,判断其是否为一个数组,如果是,则递归调用Flatten,进行降维。

# AppendToObject(对象添加新属性)

# 用法

AppendToObject是用来向指定对象添加一个额外的属性(key/value),其用法如下:

// 结果:{ id: number; name: string; }
type result = AppendToObject<{ id: number; }, 'name', string>

# 实现方式

type basicKeyType = string | number | symbol
type AppendToObject<T, K extends basicKeyType, V> = {
  [P in keyof T | K]: P extends keyof T ? T[P] : V
}

代码详解:

  • basicKeyType:在JavaScript中,因为一个对象的属性只能是stringnumber或者symbol这三种类型,所以我们限定K必须满足此条件。
  • keyof T | K:这里表示keyof T的联合类型和K,组合成一个新的联合类型。

# Absolute(绝对值)

# 用法

Absolute是用来取一个数的绝对值的,其用法如下:

// 结果:"531"
type result = Absolute<-531>

# 实现方式

type NumberLike = number | string
type Absolute<T extends NumberLike> =  `${T}` extends `-${infer N}` ? N : `${T}`

代码详解:

  • NumberLike:我们认为'1'1都是一个合法的数字,所以定义一个辅助的NumberList联合类型。
  • ${T} extends -${infer N}:这里判断我们传递的数字是否为负数,如果是则直接取其正数部分,否则直接返回。

注意:这里说到的取绝对值,最后的结果之所以是一个字符串类型,是因为TS对递归次数有限制。如果你想要真正的数字类型,可以考虑实现一个MakeArray辅助方法,使用此方法可以将字符串类型的数字,转换成一个真正的数字类型,如下:

type MakeArray<N extends string, T extends any[] = []> =
  N extends `${T['length']}`
  ? T
  : MakeArray<N, [...T, 0]>

// 结果:3
type result = MakeArray<'3'>['length']

# StringToArray(字符串转数组)

# 用法

StringToArray是用来将一个字符串转换成一个数组的,其用法如下:

// 结果:['h', 'e', 'l', 'l', 'o']
type result = StringToArray<'hello'>

# 实现方式

type StringToArray<
  S extends string,
  U extends any[] = []
> = S extends `${infer Char}${infer R}`
      ? StringToArray<R, [...U, Char]>
      : U

代码详解:StringToArray的实现主要是使用了递归的思想,它每次拿到字符串中一个字符,然后存入一个辅助数组中,当字符串为空时,直接返回这个辅助数组。

# StringToUnion(字符串转联合类型)

# 用法

在实现StringToArray后,我们能够很容易实现StringToUnion,其用法如下:

// 结果:'h' | 'e' | 'l' | 'l' | 'o'
type result = StringToUnion<'hello'>

# 实现方式

// way1: 递归思想
type StringToUnion<
  S extends string
> = S extends `${infer Char}${infer R}`
      ? Char | StringToUnion<R>
      : never
// way2: 借用StringToArray
type StringToUnion<S extends string> = StringToArray<S>[number]

代码详解:StringToArray<S>返回的是一个数组,T[number]表示对一个数组进行数字类型索引迭代,其迭代结果是每个元素组合成的一个联合类型。

# MergeType(类型合并)

# 用法

MergeType是用来合并两个类型,如果有重复的字段类型,则第二个的字段类型覆盖第一个的,其用法如下:

type Foo = {
  a: number;
  b: string;
}
type Bar = {
  b: number;
}

// 结果:{ a: number; b: number; }
type result = MergeType<Foo, Bar>

# 实现方式

type MergeType<F, S> = {
  [P in keyof F]: P extends keyof S ? S[P] : F[P]
}

# CamelCase(连字符字符串转小驼峰)

# 用法

CamelCase是用来将连字符字符串转驼峰的,其用法如下:

// 结果:fooBarBaz
type result = CamelCase<'foo-bar-baz'>

# 实现方式

type CamelCase<
  S extends string
> = S extends `${infer S1}-${infer S2}`
      ? S2 extends Capitalize<S2>
        ? `${S1}-${CamelCase<S2>}` 
        : `${S1}${CamelCase<Capitalize<S2>>}`
      : S

代码详解:CamelCase的实现,使用到了递归的思路,以上面例子为例:

type result = CamelCase<'foo-bar-baz'>

// 第一次递归调用 S满足${infer S1}-${infer S2} S2不满足extends Capitalize<S2>
S = 'foo-bar-baz' S1 = 'foo' S2 = 'bar-baz'

// 第二次递归调用 S满足${infer S1}-${infer S2} S2不满足extends Capitalize<S2>
S = 'Bar-baz' S1 = 'Bar' S2 = 'baz'

// 第三次递归调用 S不满足${infer S1}-${infer S2}
S = 'Baz'

// 结果:fooBarBaz
type result = 'foo' + 'Bar' + 'Baz' => 'fooBarBaz'

# KebabCase(字符串转连字符)

# 用法

在实现CamelCase后,我们很容易能够实现KebabCase,它是用来将驼峰形式字符串,转成连字符形式字符串的,其用法如下:

// 结果:foo-bar-baz
type result = KebabCase<'FooBarBaz'>

# 实现方式

type KebabCase<
  S extends string
> = S extends `${infer S1}${infer S2}`
      ? S2 extends Uncapitalize<S2>
        ? `${Uncapitalize<S1>}${KebabCase<S2>}`
        : `${Uncapitalize<S1>}-${KebabCase<S2>}`
      : S

# Diff(类型差异部分)

# 用法

Diff是用来获取两个类型的不同部分的,其用法如下:

type Foo = {
  id: number;
  name: string;
  age: string;
}
type Bar = {
  name: string;
  age: string;
  gender: number;
}

// 结果:{ id: number; gender: number; }
type result = Diff<Foo, Bar>

# 实现方式

type DiffKeys<T, U> = Exclude<keyof T | keyof U, keyof (T | U)>
type Diff<T, U> = {
  [k in DiffKeys<T, U>]: k extends keyof T ? T[k] : k extends keyof U ? U[k] : never
}

代码详解:

  • keyof Foo | keyof Bar:这段代码是把TU中的所有属性组合成一个新的联合类型。
  • keyof (T | U):这段代码是取TU的公共属性。
  • Exclude<K1, K2>:这段代码主要是用来从K1中排除K2,带入以上例子也就是排除掉所有公共属性。
  • Diff<T, U>:在获取到DiffKeys后,就可以迭代的方式获取到每个属性key,它所对应的类型了。

# AnyOf(数组元素真值判断)

# 用法

AnyOf用来判断数组元素,若果任意值为真,返回true;数组为空或者全部为false,才返回false,其用法如下:

// 结果:true
type result1 = AnyOf<[0, false, 0, { name: 'name' }]>
// 结果:false
type result2 = AnyOf<[0, '', false, [], {}]>

# 实现方式

type FalseType = 0 | '' | false | [] | { [key: string]: never }
type AnyOf<T extends readonly any[]> = T[number] extends FalseType ? false : true

代码详解:因为我们就是要区分true/false,所以我们把所有为false的值全部列举出来,然后使用T[number]索引迭代,依次去跟FalseType比较。

# IsNever(是否是Never类型)

# 用法

IsNever是用来判断是否为never类型,其用法如下:

// 结果:false
type result1 = IsNever<undefined>
// 结果:true
type result2 = IsNever<never>

# 实现方式

type IsNever<T> = T[] extends never[] ? true : false

# IsUnion(是否联合类型)

# 用法

IsUnion是用来判断一个类型是否为联合类型的,其用法如下:

// 结果:true
type result1 = IsUnion<string|number|boolean>
// 结果:false
type result2 = IsUnion<string>

# 实现方式

type IsUnion<T, U = T> =
  T extends T
  ? [Exclude<U, T>] extends [never]
    ? false
    : true
  : never

代码详解:上面的实现虽然代码不多,但可能无法一下子就弄明白,为了更好的理解这种实现方式,我们来看如下两个案例分析:

// 案例一
type T = string | number
step1: string | number extends string | number
step2: string extends string | number => [number] extends [never] => true
step3: number extends string | number => [string] extends [never] => true
step4: true | true
result: true

// 案例二
type T = string
step1: string extends string
step2: [never] extends [never] => false
result: false

根据之前我们学到的分布式条件类型知识,T extends T的时候,会把T进行子类型分发。

如案例一的step3step4,在分发后会把每次迭代的结果联合起来,组合成最终的结果。

# ReplaceKeys(类型替换)

# 用法

ReplaceKeys是用来在一个类型中,使用指定的Y类型来替换已经存在的T类型的,其用法如下:

// 结果:{ id: number; name: boolean; }
type result = ReplaceKeys<{ id: number; name: string; }, 'name', { name: boolean; }>

# 实现方式

type ReplaceKeys<U, T, Y> = {
  [P in keyof U]:
    P extends T
      ? P extends keyof Y
        ? Y[P]
        : never
      : U[P]
}

# RemoveIndexSignature(移除索引签名)

# 用法

RemoveIndexSignature是用来移除一个类型中的索引签名的,其用法如下:

type Foo = {
  [key: string]: any;
  foo(): void;
}

// 结果:{ foo(): void; }
type result = RemoveIndexSignature<Foo>

# 实现方式

type NeverIndex<P> = string extends P ? never : number extends P ? never : P
type RemoveIndexSignature<T> = {
  [P in keyof T as NeverIndex<P>]: T[P]
}

代码详解:

  • NeverIndex:因为索引签名有一个特点,为string或者number类型,所以我们通过string extends P或者number extends P的形式排除此索引签名。
  • as NeverIndex<P:在之前的案例中,我们介绍过as的用法,在这里有加工再次断言的意思。在使用in操作符进行迭代时,对每一个P再使用NeverIndex加工一下,如果是索引签名,这里的结果为never,为never时表示跳过当前迭代,进而达到排除索引签名的目的。

# 困难

# Currying(柯里化)

# UnionToIntersection(元组取交集)

在实现UnionToIntersection之前,我们先来回顾一下TS&符号的作用:

// 结果:never
type result1 = 1 & 'foo' & true
// 结果:{ a: number; b: number; c: boolean; }
type result2 = { a: number; b: number; } & { b: string | number; c: boolean; }
// 结果:(a: boolean | number) => string
type result3 = ((a: boolean) => string | number) & ((a: number) => string)

案例解析:

  • 案例一:因为1foo以及true,没有交集部分,所以这里结果为never
  • 案例二:对于ac属性而言,它们只存在于自身类型,所以交集部分是自身;对于b属性而言,它在两个类型中都存在,且其属性的类型存在交集部分,既:number
  • 案例三:对于函数的交叉类型,我们从函数参数、函数返回值这两个部分来说明。对于函数参数而言,取其联合类型;对于函数返回值而言,取其交叉类型。

从以上几个案例中可以看出,TS中的&符号是取交集的意思,也叫交叉类型

# 用法

UnionToIntersection所做的事情和&符号是一样的,其用法如下:

// 结果:never
type result1 = UnionToIntersection<1 | 'foo' | true>
// 结果:{ a: number; b: number; c: boolean; }
type result2 = UnionToIntersection<{ a: number; b: number; } | { b: string | number; c: boolean; }>
// 结果:(a: boolean | number) => string
type result3 = UnionToIntersection<((a: boolean) => string | number) | ((a: number) => string)>

# 实现方式

type UnionToIntersection<U> = 
  (U extends any 
    ? (x: U) => any 
    : never
  ) extends (x: infer V) => any 
    ? V
    : never

代码详解:

  • U extends any ? X : Y: 这里把U类型处理成(x: U) => any的函数类型。
  • T extends (x: infer V) => any ? V : never:这里的T就是上一步的函数类型,如果extends成立,则返回V,此时的V必然满足U & V

# RequiredKeys(所有必填字段)

# 用法

RequiredKeys是用来返回一个类型中所有必填字段,其用法如下:

type Person = {
  name: string;
  age: number;
  sex?: undefined;
  address?: string;
}

// 结果:'name' | 'age'
type result = RequiredKeys<Person>

# 实现方式

type RequiredKeys<T> = {
  [P in keyof T]: T extends Record<P,T[P]> ? P : never
}[keyof T]

代码详解:

  • T extends Record<P,T[P]>Record之前已经实现过,这里不在赘述,理解这段代码,可以参考如下案例:
// 第一步 P = 'name'
T extends { name: string; } => 'name'
// 第二步 p = 'age'
T extends { age: number; } => 'age'
// 第三步 p = 'sex'
T extends { sex?: undefined; } => never
// 第四步 p = 'address'
T extends { address?: string; } => never

在经过以上四个步骤后,得到的新类型为:

type T = {
  name: 'name';
  age: 'age';
  sex?: never;
  address?: never;
}
  • T[keyof T]keyof T得到所有的属性,然后根据属性取其类型。
// keyof T的结果
type P = 'name' | 'age' | 'sex' | 'address'

// T[P]的结果,类型为never自动过滤
type result = 'name' | 'age' | never | never => 'name' | 'age'

# GetRequired(必填字段组成的类型)

# 用法

GetRequired是用来取一个类型中那些由必填字段组成的一个新类型的,其用法如下:

type Person = {
  name?: string;
  age: number;
  address?: string;
  sex: undefined;
}

// 结果:{ age: number; sex: undefined; }
type result = GetRequired<Person>

# 实现方式

RequiredKeys的基础上,能够很容易的实现GetRequired

type GetRequired<T> = {
  [P in RequiredKeys<T>]: T[P]
}

# OptionalKeys(所有可选字段)

OptionalKeysRequiredKeys所做的事情相反,其获取的是所有可选字段。

# 用法

type Person = {
  name: string;
  age: number;
  sex?: undefined;
  address?: string;
}

// 结果:'sex' | 'address'
type result = OptionalKeys<Person>

# 实现方式

type OptionalKeys<T> = {
  [P in keyof T]: T extends Record<P, T[P]> ? never : P
}[keyof T]

代码详解:从上面代码中可以看出,它和RequiredKeys实现思路是一样的,区别只是在extends关键词后面的处理不同。

# GetOptional(可选字段组成的类型)

# 用法

在实现了OptionalKeys后,我们来实现其对应的GetOptional,其对应方法使用方式如下:

type Person = {
  name: string;
  age: number;
  sex?: undefined;
  address?: string;
}

// 结果:{ sex?: undefined; address?: string; }
type result = GetOptional<Person>

# 实现方式

type GetOptional<T> = {
  [P in OptionalKeys<T>]?: T[P]
}

# CapitalizeWords(所有单词首字母大写)

# 用法

CapitalizeWords是用来把一个字符串中所有单词,变为大写字母的,其中这个字符串以固定的分隔符分割,用法如下:

// 结果:'Foobar'
type t1 = CapitalizeWords<'foobar'>
// 结果:'Foo Bar.Hello,World'
type t2 = CapitalizeWords<'foo bar.hello,world'>

# 实现方式

type CapitalizeWords<
  S extends string,
  R extends string = ''
> =  S extends `${infer left}${infer split}${infer right}`
      ? split extends ' ' | '.' | ','
         ? CapitalizeWords<Capitalize<right>, `${R}${left}${split}`>
         : CapitalizeWords<right, `${R}${left}${split}`>
      : Capitalize<`${R}${S}`>

代码详解:在以上实现方法中,我们借用辅助字符串来实现,以第二个例子为例,详细解析分析如下:

// 第一次迭代
R = '' left = 'f' split = 'o' right = 'oo bar.hello,world'
split不满足条件,递归调用

// 第二次迭代
R = 'fo' left = 'o' split = ' ' right = 'bar.hello,world'
split满足条件,递归调用

// 第三次迭代
R = 'foo ' left = 'b' split = 'a' right = 'r.hello,world'
split不满足条件,递归调用

... 省略

// 最后一次迭代
R = 'foo Bar.Hello,Worl' S = 'd'
S不满足条件

最后结果:R + S = Capitalize<'foo Bar.Hello,worl' + 'd'> => 'Foo Bar.hello,world'

# CamelCase(下划线字符串转小驼峰)

# 用法

中级章节实现不同,此章节中CamelCase是用来将下划线字符串转小驼峰的,其用法如下:

// 结果:'fooBarHelloWorld'
type result = CamelCase<'foo_bar_hello_world'>

# 实现方式

type CamelCase<
  S extends string
> = S extends `${infer left}_${infer char}${infer right}`
      ? `${Lowercase<left>}${Uppercase<char>}${CamelCase<right>}`
      : Lowercase<S>

# ParsePrintFormat(获取字符串格式化参数)

# 用法

ParsePrintFormat是用来获取字符串格式化参数的,其用法如下:

// 参数映射表
type ControlMap = {
  'c': 'char',
  's': 'string',
  'd': 'dec',
  'o': 'oct',
  'h': 'hex',
  'f': 'float',
  'p': 'pointer'
}

// 结果:['string', 'dec']
type result = ParsePrintFormat<'Hello %s: score is %d'>

# 实现方式

type ControlMap = {
  'c': 'char',
  's': 'string',
  'd': 'dec',
  'o': 'oct',
  'h': 'hex',
  'f': 'float',
  'p': 'pointer'
}

type ParsePrintFormat<
  S extends string,
  R extends string[] = []
> = S extends `${infer S1}%${infer Char}${infer S2}`
    ? Char extends keyof ControlMap
      ? ParsePrintFormat<S2, [...R, ControlMap[Char]]>
      : ParsePrintFormat<S2, R>
    : R

代码详解:在以上实现方法中,借用了辅助数组的思想,拿上面案例来说,具体迭代分析如下:

// 第一次迭代
S满足条件 R = [] S1 = 'Hello ' Char = 's' S2 = ': score is %d'

// 第二次迭代
S满足条件 R = ['string']  S1 = ': score is ' Char = 'd' S2 = ''

// 最后一次迭代
S不满足条件 R = ['string', 'dec']

// 结果
result = R = ['string', 'dec']

# IsAny和NotAny

# 用法

IsAny是用来判断一个类型是否为any的,NotAny和它做的事情相反。

type t1 = IsAny<undefined> // false
type t2 = IsAny<never>     // false
type t3 = IsAny<any>       // true

type t4 = NotAny<undefined> // true
type t5 = NotAny<never>     // true
type t6 = NotAny<any>       // false

# 实现方式

type IsAny<T> = 0 extends (1&T) ? true : false
type NotAny<T> = true extends IsAny<T> ? false : true

代码详解:1 & T的结果只能是:1never或者any。当使用0 extends这三个结果的时候,只有any判断为真。

// 结果:false
type t1 = 0 extends 1 ? true : false
// 结果:false
type t2 = 0 extends never ? true : false
// 结果:true
type t3 = 0 extends any ? true : false

# Get(字符串路径取值)

# 用法

Get是用来进行字符串路径取值的,其用法如下:

type Data = {
  foo: {
    bar: {
      value: 'foobar',
      count: 6,
    },
    include: true,
  },
  hello: 'world'
}

// 结果:world
type t1 = Get<Data, 'hello'>
// 结果:foobar
type t2 = Get<Data, 'foo.bar.value'>
// 结果: never
type t3 = Get<Data, 'no.exits'>

# 实现方式

type Get<T, S extends string> =
  S extends `${infer S1}.${infer S2}`
    ? S1 extends keyof T
      ? Get<T[S1], S2>
      : never
    : S extends keyof T
      ? T[S]
      : never

代码详解:对于Get的实现,主要分为两部分:含有.符号的字符串和不含.符号的字符串。

  • 不含有.符号的字符串:对于这种情况,我们只需要判断它是否为T类型中的某个key,如果是则直接取值;如果不是,则返回never
  • 含有.符号的字符串:对于这种情况,我们先判断.符号左侧部分是否满足为T类型的某个key,如果满足则递归调用Get;如果不满足,则直接返回never

# StringToNumber(字符串数字转数字)

# 用法

StringToNumber是用来将字符串形式的数字转换成真正数字类型数字的,其用法如下:

// 结果:123
type result = StringToNumber<'123'>

# 实现方式

JavaScript中,我们可以很方便的调用Number()方法或者parseInt()方法来将字符串类型的数字,转换成数字类型的数字。但在TS中,并没有这样的方法,需要我们来手动实现。

StringToNumber的实现并不容易理解,我们需要将其进行拆分,一步步来完善,其实现思路如下:

  • 第一步:可以很容易获取字符串'123'中每一位字符,我们将其存储在辅助数组T中,如下:
type StringToNumber<S extends string, T extends any[] = []> = 
  S extends `${infer S1}${infer S2}`
    ? StringToNumber<S2, [...T, S1]>
    : T

// 结果:['1', '2', '3']
type result = StringToNumber<'123'>
  • 第二步:我们需要将单个字符串类型的数字,转换成真正数字类型的数字,可以借助中间数组来帮忙,例如:
'1' => [0]['length'] => 1
'2' => [0,0]['length'] => 2
'3' => [0,0,0]['length'] = 3
...
'9' => [0,0,0,0,0,0,0,0,0]['length'] => 9

根据以上规律,我们封装一个MakeArray方法,它的实现代码如下:

type MakeArray<N extends string, T extends any[] = []> = N extends `${T['length']}` ? T : MakeArray<N, [...T, 0]>

type t1 = MakeArray<'1'> // [0]
type t2 = MakeArray<'2'> // [0, 0]
type t3 = MakeArray<'3'> // [0, 0, 0]
  • 第三步:现在有了百位,十位和个位的数字,我们应该运用算术把它们按照一定的规律累加起来,如下:
const arr = [1, 2, 3]
let target = 0

// 第一次迭代
target = 10 * 0 + 1 = 1
// 第二次迭代
target = 10 * 1 + 2 = 12
// 第三次迭代
target = 10 * 12 + 3 = 123
// 迭代规律
target = 10 * target + N

根据以上思路,我们还需要一个乘十的工具函数,对应到实际需求,就是需要把一个数组copy十次,因此我们封装一个Multiply10工具,其实现代码如下:

type Multiply10<T extends any[]> = [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]

type result = Multiply10<[1]> // [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
  • 第四步:根据前几步的分析,把所有东西串联起来,StringToNumber完整实现代码如下:
type Digital = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
type Multiply10<T extends any[]> = [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
type MakeArray<N extends string, T extends any[] = []> = N extends `${T['length']}` ? T : MakeArray<N, [...T, 0]>

type StringToNumber<S extends string, T extends any[] = []> = 
  S extends `${infer S1}${infer S2}`
    ? S1 extends Digital
      ? StringToNumber<S2, [...Multiply10<T>, ...MakeArray<S1>]>
      : never
    : T['length']
  • 第五步:为了更好的理解递归的过程,我们拆解成如下步骤来说明:
type result = StringToNumber<'123'>

// 第一次递归,S满足${infer S1}${infer S2}, S1满足Digital
S = '123' S1 = '1' S2 = '23' T = [0] T['length'] = 1

// 第二次递归,S满足${infer S1}${infer S2}, S1满足Digital
S = '23'  S1 = '2' S2 = '3' T = [0,....0] T['length'] = 10

// 第三次递归,S满足${infer S1}${infer S2}, S1满足Digital
S = '3'  S1 = '3' S2 = '' T = [0,....0] T['length'] = 120

// 第四次递归,S不满足${infer S1}${infer S2} T['length']取值
S = '' T = [0,....0] T['length'] = 123

// 结果:
type result = StringToNumber<'123'> // 123

# FilterOut(数组元素过滤)

# 用法

FilterOut是用来从数组中移除指定元素的,其用法如下:

// 结果:[2]
type result = FilterOut<[1, 'a', 2], 'a' | 1>

# 实现方式

type FilterOut<
  T extends any[],
  F,
  K extends any[] = []
> = T extends [infer R, ...infer args]
      ? [R] extends [F]
        ? FilterOut<args, F, [...K]>
        : FilterOut<args, F, [...K, R]>
      : K

代码详解:

  • 第一步:我们借用赋值函数来存放最后的结果。
  • 第二步:迭代数组T,拿每一个元素去和指定的F进行判断,如果RF的子类型,则不添加此元素到结果数组中,反之添加。
  • 第三步:当迭代完毕时,直接返回结果数组K

# TupleToEnum(元组转枚举)

# 用法

TupleToEnum是用来将元组转换为枚举的,其用法如下:

const OperatingSystem = ['macOs', 'Windows', 'Linux'] as const
type Expected1 = {
  readonly MacOs: 'macOs';
  readonly Windows: 'Windows';
  readonly Linux: 'Linux'
}

// 结果:Expected1
type result1 = TupleToEnum<typeof OperatingSystem>

# 实现方式

在实现TupleToEnum之前,我们先来实现TupleKeys,它是用来获取所有元组索引组合成的联合类型的。

type TupleKeys<
  T extends readonly any[]
> = T extends readonly [infer R, ...infer args]
      ? TupleKeys<args> | args['length']
      : never

// 结果:0 | 1 | 2
type keys = TupleKeys<typeof OperatingSystem>

在有了以上keys后,我们就能实现TupleToEnum了,如下:

type TupleToEnum<T extends readonly string[]> = {
  readonly [K in TupleKeys<T> as Capitalize<T[K]>]: T[K]
}

# Format(字符串格式化函数类型)

# LengthOfString(字符串的长度)

# Join(字符串拼接)

# DeepPick(深层次Pick)

# Camelize(类型属性键转小驼峰)

# 地狱

撰写中...

最后更新时间: 2/28/2023, 8:33:37 PM