Type-Challenges

介绍

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

Type-Challenges中,可以从简单(easy)、中等(medium)、困难(hard)以及地狱(extreme)难度,循序渐进的学习TypeScript高级技巧。

如果你需要选择其它的方向来深入学习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来迭代这个联合类型,如下:

// 仅演示使用, K为每次迭代的项
K in 'name' | 'age' | 'sex'
K = 'name' // 第一次迭代结果
K = 'age'  // 第二次迭代结果
K = 'sex'  // 第三次迭代结果

根据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

// js中的迭代
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或者void来表示其返回值,如下:

// never更适合用来表示永远没有返回值的函数
function handlerError(message: string): never {
  throw new Error(message)
}
// void适合用来表示返回值为空的函数
function handlerError(message: string): void {
  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的类型被收缩在一个联合类型的范围内。例如: U extends 'name' | 'age',则表示U只能为name或者age二者其中之一。

条件类型

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

T extends U ? 'Y' : 'N'

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

type result1 = true extends boolean ? true : false                    // true
type result2 = 'name' extends 'name' | 'age' ? true : false           // true
type result3 = [1, 2, 3] extends { length: number; } ? true : false   // true
type result4 = [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 result = Extract<type1, type2>

// 推理步骤
'name'|'age' extends 'name'|'address'|'sex' ? T : never
step1: ('name' extends 'name'|'address'|'sex' ? 'name' : never) => 'name'
step2:  ('age' extends 'name'|'address'|'sex' ? 'age' : never)   => never
result: '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类型的特点,最后的结果会剔除掉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中的infer占位更像JavaScript中的模板字符串:

// 函数的返回类型使用R占位表示
(...args: any) => info R

// 模板字符串中的值,使用变量name占位表示
const str = `hello, ${name}`

& 符号

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

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

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

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

type result = T & U

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

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

// ts v4.8.4以上版本
function merge<T, U, K extends T & U>(to: T, from: U): K {
  for (let key in from) {
    ;(to as unknown as K)[key] = from[key] as any
  }
  return to as unknown as K
}

// ts v4.8.4以下版本
function merge<T, U, K extends T & U>(to: T, from: U): K  {
  for (let key in from) {
    ;(to as K)[key] = from[key] as any
  }
  return to as K
}

const obj1 = { name: 'AAA' }
const obj2 = { age: 23 }
// js结果:{ name:'AAA'; age: 23; }
// ts结果:{ name: string; age: number; }
const result = merge(obj1, obj2)

初级

内置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'>

内置Readonly(只读)

用法

Readonly是用来让所有属性变为只读,其用法为:

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

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

实现方式

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

TupleToObject(元组转对象)

用法

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

// 类型:readonly ['msg', 'name']
const 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]
// 占位实现方式
type First<T extends any[]> = T extends [infer R, ...infer L] ? R : never

代码详解:

  • T extends []:用来判断T是否是一个空数组。
  • T[0]:根据下标取数组第一个元素。
  • infer R: 表示数组第一个元素的占位。
  • ...infer L: 表示数组剩余元素的占位。

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'])。

内置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'

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 extends Promise<any>
      ? PromiseType<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 Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

// 简单版
type MyIncludes<T extends readonly any[], U> = U extends T[number] ? true : false
// 完善版
type MyIncludes<T extends readonly any[], U> = 
  T extends [infer R, ...infer L]
    ? Equal<R, U> extends true
      ? true
      : MyIncludes<L, U>
    : false

代码详解:

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

Push(数组push方法)

用法

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

实现方式

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

Unshift(数组unshift方法)

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

用法

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

实现方式

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

内置Parameters(函数的参数类型)

用法

Parameters是用来获取一个函数的参数类型的,其中获取的结果是一个元组,用法如下:

const add = (a: number, b: string): void => {}
// [number, string]
type result = MyParameters<typeof add>

实现方式

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

内置Partial(可填)和内置Required(必填)

TIP

此题不属于type-challenges类型挑战题

用法

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]
}

内置Record(构造)

TIP

此题不属于type-challenges类型挑战题

用法

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(交集)

TIP

此题不属于type-challenges类型挑战题

用法

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

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

// 结果:'age'|'address'
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'

中级

内置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来代替真正的返回类型。

内置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中选取这两个字段,组合成一个新的类型。

Readonly(按需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

实现方式

// ts v4.4+版本可直接用
type Readonly<T, K extends keyof T = keyof T> = T & {
  readonly [P in K]: T[P]
}
// ts v4.5+版本必须用
type Readonly<T, K extends keyof T = keyof T> = Omit<T, K> & {
  readonly [P in K]: T[P]
}

代码详解:

  • K extends keyof T = keyof T:如要传递了K,那么只能是T中已经存在的属性,不存在则报错;如果不传递,则默认值为keyof 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 readonly [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'

Chainable(可串联构造器)

用法

Chainable是用来让一个对象可以进行链式调用的,用法如下:

type Expected = {
  foo: number
  bar: {
    value: string
  }
  name: string
}
declare const obj: Chainable<{}>
// 结果:Expected
const result = obj
  .options('foo', 123)
  .options('bar', { value: 'Hello' })
  .options('name', 'TypeScript')
  .get()

实现方式

type Chainable<T> = {
  options<K extends string, V>(key: K, value: V): Chainable<T & {[k in K]: V}>
  get(): T
}

代码详解:

  • {[k in K]: V}:每次调用options时,把key/value构造成一个对象,例如:{ foo: 123 }
  • T & U:此处使用到&关键词,用来合并TU两个对象中的所有key
  • Chainable<>:递归调用Chainable,赋予新对象以链式调用的能力。

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(数组Pop方法)

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

用法

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

实现方式

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

PromiseAll返回类型

用法

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

const result1 = PromiseAll([1, 2, 3] as const)
const result2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const result3 = PromiseAll([1, 2, Promise.resolve(3)])
const result4 = PromiseAll<Array<number | Promise<number>>>([1, 2, 3])

// 结果1: Promise<[1, 2, 3]>
type t1 = typeof result1
// 结果2: Promise<[1, 2, number]>
type t2 = typeof result2
// 结果3: Promise<[number, number, number]>
type t3 = typeof result3
// 结果4: Promise<number[]>
type t4 = typeof result4

实现方式

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

// Awaited为内置类型
type PromiseAllType<T> = Promise<{
  [P in keyof T]: Awaited<T[P]>
}>
declare function PromiseAll<T extends any[]>(values: readonly [...T]): PromiseAllType<T>

代码详解:

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

LookUp(查找)

用法

LookUp是用来根据类型值查type找类型的,其用法如下:

interface Cat {
  type: 'cat'
  color: 'black' | 'orange' | 'gray'
}
interface Dog {
  type: 'dog'
  color: 'white'
  name: 'wang'
}

// 结果:Dog
type result = LookUp<Cat | Dog, 'dog'>

实现方式

type LookUp<
  U extends { type: string; },
  T extends string
> = U extends { type: T } ? U : never

代码详解:

  • U extends { type: string; }:这段代码限制U的类型必须是具有属性为type的对象。
  • U extends { type: T }:如果把T的值实际带入,为U extends { type: 'dog' },表示判断U中的type值是不是dog,是则返回U

Trim、TrimLeft以及TrimRight

TrimLeft:

TrimRight: Trim:

用法

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

type t1 = TrimLeft<' str'>  // 'str'
type t2 = Trim<' str '>     // 'str'
type 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(首字母大写)和UnCapitalize(首字母小写)

用法

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

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

实现方式

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

代码详解:

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

Replace

用法

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

// 结果1:'foofoobar'
type t1 = Replace<'foobarbar', 'bar', 'foo'>
// 结果2: foobarbar
type t2 = Replace<'foobarbar', '', 'foo'>

实现方式

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

ReplaceAll

用法

ReplaceAll是用来将字符串中指定字符全部替换的,其用法如下:

// 结果:'foofoofoo'
type t = ReplaceAll<'foobarbar', 'bar', 'foo'> 

实现方式

type ReplaceAll<
  S extends string,
  from extends string,
  to extends string
> = S extends `${infer L}${from}${infer R}`
      ? from extends ''
        ? S
        : `${ReplaceAll<L, from, to>}${to}${ReplaceAll<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函数的参数类型以及返回类型,然后把新的参数添加到参数列表,并原样返回其函数类型。

Permutation(排列组合)

用法

Permutation是用来将联合类型中的每一个类型进行排列组合,其用法如下:

// 结果1:['A', 'B'] | ['B', 'A']
type result1 = Permutation<'A' | 'B'>
// 结果2:['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
type result2 = Permutation<'A' | 'B' | 'C'>

实现方式

type Permutation<T, U = T> = 
  [T] extends [never]
    ? []
    : T extends U
      ? [T, ...Permutation<Exclude<U, T>>]
      : never

代码详解:

  • [T] extends [never]:这段代码主要是为了处理联合类型为空的情况。
  • T extends U:这段代码主要是需要使用分布式条件类型这个知识点,当T extends U成立时,在其后的判断语句中,T代表当前迭代的类型。
  • <Exclude<U, T>:因为此时的T代表当前迭代的类型,所以我们从原始联合类型中排除当前类型,然后递归调用Permutation。当TA时,递归调用Permutation<'B' | 'C'>, 此时结果为['A'] + ['B', 'C']['A'] + ['C', 'B']

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是用来取一个数的绝对值的,其用法如下:

// 结果1:"531"
type result1 = Absolute<-531>
// 结果2:"9999"
type result2 = Absolute<9_999n>

实现方式

type NumberLike = number | string | bigint
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(字符串转数组)

TIP

此题不属于type-challenges类型挑战题

用法

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]表示对一个数组进行数字类型索引迭代,其迭代结果是每个元素组合成的一个联合类型。

Merge(类型合并)

用法

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

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

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

实现方式

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

代码详解:

  • keyof F | keyof S:这段代码的含义是将FS这两个对象的键组合成一个新的联合类型。
  • P extends:这里进行了两次extends判断,其中第二次不能直接写成F[P],而应该多判断一次,当满足条件时才使用F[P],这是因为P的类型判断无法作用于:符号后面。

KebabCase(字符串转连字符)

用法

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,它所对应的类型了。
  • K extends keyof U:额外再判断一次,是因为K不能在三元表达式右侧使用。

AnyOf(数组元素真值判断)

用法

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

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

实现方式

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

代码详解:因为我们就是要区分true/false,所以我们把所有为false的值全部列举出来,然后使用T[number]索引迭代,依次去跟FalsyType比较,其中{ [key: string]: never }表示空对象{}

IsNever(是否是Never类型)

用法

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

// 结果1:false
type result1 = IsNever<undefined>
// 结果2:true
type result2 = IsNever<never>
// 结果3:false
type result3 = IsNever<never | string>

实现方式

export type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

// way1: 类型数组
type IsNever<T> = T[] extends never[] ? true : false
// way2: 数组值
type IsNever<T> = [T] extends [never] ? true : false
// way3: 值比较
type IsNever<T> = Equal<T, never>

IsUnion(是否联合类型)

用法

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

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

实现方式

// way1: 排除法
type IsUnion<T, U = T> =
  [T] extends [never]
    ? false
    : T extends U
      ? [Exclude<U, T>] extends [never]
        ? false
        : true
      : false
// way2: 正反对比法
type IsUnion<T, U = T> = 
  (T extends U
    ? U extends T
      ? true
      : unknown
    : false
  ) extends true ? false : true

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

// 案例一
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 U的时候,会把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 CheckIndexSignature<T, P> = P extends T ? true : false
type RemoveIndexSignature<T, K = PropertyKey> = {
  [P in keyof T as (CheckIndexSignature<P, K> extends false ? P : never)] : T[P]
}

代码详解:

  • CheckIndexSignature:因为索引签名有一个特点,为string | number | symbol,所以我们通过P extends T ? true : false形式排除此索引签名。其原理如下:
type FooKeys = string | 'foo'

// 第一次迭代
example1: T = 'foo', P = string | number | symbol
step1: (string | number | symbol) extends 'foo' ? true : false
step2: (string extends 'foo' ? true : false) |
       (number extends 'foo' ? true : false) |
       (symbol extends 'foo' ? true : false) |
step3: false | false | false
step4: false

// 第二次迭代
example2: T = string, P = string | number | symbol
step1: (string | number | symbol) extends string ? true : false
step2: (string extends string ? true : false) |
       (number extends string ? true : false) |
       (symbol extends string ? true : false) |
step3: true | false | false
step4: true | false
step5: boolean
  • as xxx:在之前的案例中,我们介绍过as的用法,在这里有加工再次断言的意思。在使用in操作符进行迭代时,对每一个P再使用CheckIndexSignature加工一下,如果是索引签名,这里的结果为never,为never时表示跳过当前迭代,进而达到排除索引签名的目的。
// 第一次迭代 P = 'foo'
   CheckIndexSignature<P, K> extends false ? P : never
=> false extends false ? P : never
=> P

// 第二次迭代 P = string
   CheckIndexSignature<P, K> extends false ? P : never
=> boolean extends false ? P : never
=> never

// 最终结果
type result = { foo(): void; }

PercentageParser(百分比解析)

用法

PercentageParser是用来解析百分比字符串的,其用法如下:

type result1 = PercentageParser<'+85%'> // ['+', '85', '%']
type result2 = PercentageParser<'-85%'> // ['-', '85', '%']
type result3 = PercentageParser<'85'>   // ['', '85', '']

实现方式

type CheckPrefix<S extends string> = S extends '+' | '-' ? S : never
type CheckSuffix<S extends string> = S extends `${infer L}%` ? [L, '%'] : [S, '']

type PercentageParser<S extends string> = 
  S extends `${CheckPrefix<infer L>}${infer R}`
    ? [L, ...CheckSuffix<R>]
    : ['', ...CheckSuffix<S>]

代码详解:

  • CheckPrefix是用来处理百分比字符串前面的符号的,如果存在+或者-,则原样返回,如果不存在则返回never,表示没有符号。
  • CheckSuffix是用来处理百分比字符串后面的百分比符号的,如果存在,则返回一个数组(最后一项固定为百分比符号);如果不存在,则返回的数组最后一个元素固定为空字符串。

DropChar(移除字符)

用法

DropChar是用来在字符串中移除指定字符的,其用法如下:

// 结果:butterfly!
type result = DropChar<' b u t t e r f l y ! ', ' '>

实现方式

type DropChar<
  S extends string,
  C extends string
> = C extends ''
  ? S
  : S extends `${infer L}${C}${infer R}`
    ? DropChar<`${L}${R}`, C>
    : S

代码详解:DropCharReplaceAll的实现思路非常相似,首先需要判断待移除的字符是不是空字符串,如果是,则直接返回原始字符串;如果不是,先判断字符串中是否包含待移除的字符,包含则递归调用;不包含则直接返回原始字符串。

MinusOne(减一)

MinusOne是用来实现数字减一的,其用法如下:

用法

// 结果:99
type result = MinusOne<100>

实现方式

type MinusOne<
  N extends number,
  T extends any[] = []
> = N extends T['length']
  ? T extends [infer F, ...infer Rest]
    ? Rest['length']
    : never
  : MinusOne<N, [0, ...T]>

代码详解:在实现MinusOne的时候,借用了一个空数组,首先判断数组的长度是否等于传递的数字N,如果相等则从数组中随意移除一位,然后返回剩下数组的长度即可;如果不相等,则往数组中添加一个元素,再递归调用MinusOne

注意:由于TS在递归调用时存在最大递归调用次数,所以对于比较大的数字会提示错误。

PickByType(根据类型选取)

用法

PickByType是用来根据类型选取属性的,其用法如下:

interface Model {
  name: string
  count: number
  isReadonly: boolean
  isEnable: boolean
}
// 结果:{ isReadonly: boolean, isEnable: boolean }
type result = PickByType<Model, boolean>

实现方式

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

代码详解:PickByType的实现,可以使用as进行第二次断言,当类型满足时就返回当前迭代的P,不满足类型时就返回never,因为never最后会被排除,所以最后的迭代结果只有满足类型的键。

StartsWith(字符串startsWith方法)

用法

StartsWith是用来实现JavaScript中字符串的startsWith功能,其用法如下:

// 结果:true
type result = StartsWith<'abc', 'ab'>

实现方式

type StartsWith<
  S extends string,
  C extends string
> = S extends `${C}${string}` ? true : false

EndsWith(字符串endsWith方法)

用法

EndsWith是用来实现JavaScript中字符串的endsWith功能,其用法如下:

// 结果:true
type result = endsWith<'abc', 'bc'>

实现方式

type EndsWith<
  S extends string,
  C extends string
> = S extends `${string}${C}` ? true : false

PartialByKeys(按需可选)

用法

PartialByKeys是用来实现按需可选的,其用法如下:

interface User {
  name: string
  age: number
  address: string
}
interface UserPartialName {
  name?: string,
  age: number
  address: string 
}

// 结果:UserPartialName
type result = PartialByKeys<User, 'name'>

实现方式

type CopyKeys<T> = {
  [P in keyof T]: T[P]
}
type PartialByKeys<
  T,
  K extends keyof T = keyof T
> = CopyKeys<Omit<T, K> & {
  [P in K]?: T[P]
}>

代码详解:

  • Omit部分:根据之前介绍的Omit的知识,Omit<T, K>表示从T中剔除含有K的类型。
  • CopyKeys部分:如果不使用CopyKeys,最后的结果为T & U形式,它实际上与使用CopyKeys的结果是一样的。这里使用CopyKeys,很大程度上是为了测试。
// 使用CopyKeys,结果为true;不使用,结果为false
type result1 = Equal<PartialByKeys<User, 'name'>, UserPartialName>

RequiredByKeys(按需必填)

在实现PartialByKeys后,很容易按照相同的思路去实现RequiredByKeys

用法

RequiredByKeys是用来实现按需必填的,其用法如下:

interface User {
  name?: string
  age?: number
  address?: string
}

interface UserRequiredName {
  name: string
  age?: number
  address?: string 
}

// 结果:UserRequiredName
type result = RequiredByKeys<User, 'name'>

实现方式

type CopyKeys<T> = {
  [P in keyof T]: T[P]
}
type RequiredByKeys<
  T,
  K extends keyof T = keyof T
> = CopyKeys<Omit<T, K> & {
  [P in K]-?: T[P]
}>

代码详解:实现思路参考PartialByKeys

Mutable(可改)

用法

Mutable是用来让所有属性变为可改的(移除readonly关键词),其用法为:

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

实现方式

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

代码解读:

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

OmitByType(按类型移除)

OmitByType的实现思路和PickByType类似。

用法

OmitByType是用来按照类型移除的,其用法如下:

interface Model {
  name: string
  count: number
  isReadonly: boolean
  isEnable: boolean
}
interface ModelOmitBoolean {
  name: string;
  count: number
}

// 结果:ModelOmitBoolean
type result = OmitByType<Model, boolean>

实现方式

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

代码解析:实现思路参考PickByType

ObjectEntries

用法

ObjectEntries是用来实现JavaScript中的Object.entries()方法,其用法如下:

interface Model {
  name: string;
  age: number;
  locations?: string[] | null;
}
type ModelEntries = ['name', string] | ['age', number] | ['locations', string[] | null];

// 结果:ModelEntries
type result = ObjectEntries<Model>

实现方式

type RemoveUndefined<T> = [T] extends [undefined] ? T : Exclude<T, undefined>
type ObjectEntries<T> = {
  [P in keyof T]-?: {} extends Pick<T, P> ? [P, RemoveUndefined<T[P]>] : [P, T[P]]
}[keyof T]

代码详解:

  • RemoveUndefined:当T仅为undefined,表示原始类型就是undefined; 当T为联合类型时,移除联合类型中的undefined
  • [P in keyof T]-?: 表示移除可选属性。
  • {} extends Pick<T, P>: 判断当前的P是否为可选属性,是的话就是使用RemoveUndefined移除其中的undefined,否则取原始类型。
type Person = {
  name?: string 
}
// 结果都为true
type result1 = {} extends Person ? true : false
type result2 = { name: string; } extends Person ? true : false

Shift(数组shift方法)

用法

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

实现方式

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

TupleToNestedObject(元组转嵌套对象)

用法

TupleToNestedObject是用来将元组转成嵌套对象的,其用法如下:

// 结果:{ a: { b: string; } }
type result = TupleToNestedObject<['a', 'b'], string>

实现方式

type TupleToNestedObject<T extends any[], U> =
  T extends [infer F, ...infer R]
    ? { [P in F & string]: TupleToNestedObject<R, U> }
    : U

代码详解:

  • F & string: 等价于如下代码:
F & string = F extends string ? F : never

Reverse

用法

Reverse是用来实现数组的reverse()方法的,其用法如下:

// 结果:['b', 'a']
type result = Reverse<['a', 'b']>

实现方式

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

FlipArguments(反转函数参数类型)

借助上面的Reverse方法,可以很容易实现函数参数的反转。

用法

FlipArguments是用来实现反转函数参数类型的,其用法如下:

// 结果:(a: number, b: string) => string | number
type result = FlipArguments<(a: string, b: number) => string | number>

实现方式

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

FlattenDepth(数组按深度降维)

用法

FlattenDepth是用来按深度进行数组降维的,其用法如下:

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

实现方式

type FlattenDepth<
  T extends any[],
  D extends number = 1,
  U extends any[] = []
> = T extends [infer F, ...infer R]
  ? U['length'] extends D
    ? T
    : F extends any[]
      ? [...FlattenDepth<F, D, [0, ...U]>, ...FlattenDepth<R, D>]
      : [F, ...FlattenDepth<R, D, U>]
  : T

代码详解:FlattenDepth的实现思路和Flatten基本一致,区别是按深度降维时需要一个数组去记录降维的次数(深度)。

BEM

用法

BEM是用来将字符串连接成CSS BEM格式的,其用法如下:

// 结果:'btn__primary--small' | 'btn__primary--mini' 
type result = BEM<'btn', ['primary'], ['small', 'mini']>

实现方式

type ArrayToString<
  T extends any[],
  P extends string
> = T extends [] ? '' : `${P}${T[number]}`

type BEM<
  B extends string,
  E extends string[],
  M extends string[]
> = `${B}${ArrayToString<E, '--'>}${ArrayToString<M, '__'>}`

代码详解:实现BEM的思路并不复杂,只需要记住如下两个知识点:

  • 判断是一个空数组,可以使用T extends []或者T['length'] extends 0
  • T[number]会自动迭代数组,例如:
// 结果: 'A__B' | 'A__C' | 'A__D'
type result = `A__${['B', 'C', 'D'][number]}`

InOrderTraversal(中序遍历)

先序遍历PreOrderTraversal先访问根节点,然后访问左节点,最后访问右节点。
中序遍历InOrderTraversal先访问左节点,然后访问根节点,最后访问右节点。
后序遍历PostOrderTraversal先访问左节点,然后访问右节点,最后访问根节点。

用法

InOrderTraversal是用来实现二叉树中序遍历的,其用法如下:

const tree = {
  val: 1,
  left: null,
  right: {
    val: 2,
    left: {
      val: 3,
      left: null,
      right: null,
    },
    right: null,
  },
}

// 结果: [1, 3, 2]
type result = InOrderTraversal<typeof tree>

实现方式

// 一个二叉树节点
interface TreeNode {
  val: number;
  left: TreeNode | null;
  right: TreeNode | null;
}
// 先序遍历实现
type PreOrderTraversal<
  T extends TreeNode | null
> = [T] extends [TreeNode]
  ? [T['val'], ...PreOrderTraversal<T['left']>, ...PreOrderTraversal<T['right']>]
  : []
// 中序遍历实现
type InOrderTraversal<
  T extends TreeNode | null
> = [T] extends [TreeNode]
  ? [...InOrderTraversal<T['left']>, T['val'], ...InOrderTraversal<T['right']>]
  : []
// 后序遍历实现
type PostOrderTraversal<
  T extends TreeNode | null
> = [T] extends [TreeNode]
  ? [...PostOrderTraversal<T['left']>, ...PostOrderTraversal<T['right']>, T['val']]
  : []

代码详解:

  • [T] extends [TreeNode]: 使用此形式而不用T extends TreeNode,这是因为T是一个TreeNode | null,在左侧会进行分布式条件类型,判断两次:
// 如果Tree嵌套比较深的话,ts会报错
TreeNode extends TreeNode |
null extends TreeNode
  • 遍历方式:根据先序遍历PreOrderTraversal、中序遍历InOrderTraversal、后序遍历PostOrderTraversal的定义,只需要在递归的时候处理其访问顺序即可。

FlipObject(对象键值交换)

用法

FlipObject是用来将对象的键值交换的,其用法如下:

// 结果:{ pi: 'a' }
type result = FlipObject<{ a: 'pi' }>

实现方式

type BasicType = string | number | boolean
type FlipObject<T extends Record<string, BasicType>> = {
  [P in keyof T as `${T[P]}`]: P
}

Fibonacci(斐波那契数列)

菲波那切数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144...

用法

Fibonacci是用来实现斐波那契数列的,用法如下:

type result = Fibonacci<5>

实现方式

type Fibonacci<
  N extends number,
  Index extends any[] = [1],
  Prev extends any[] = [],
  Current extends any[] = [1]
> = Index['length'] extends N
  ? Current['length']
  : Fibonacci<N, [...Index, 1], Current, [...Prev, ...Current]>

代码详解:

  • Index:标记当前数列是第几项,从1开始。
  • Prev:存储数列上一次计算的值,从0开始。
  • Current: 标记当前数列的值,根据数列的特点,第N项的值,等于N - 1项 + N - 2项的值,即:Current = [...Prev, ...Current]

AllCombinations(全排列)

用法

AllCombinations是用来列举全部排列组合可能性的,其用法如下:

// 结果:'' | 'A' | 'AB' | 'B' | 'BA'
type result = AllCombinations<'AB'>

实现方式

type StringToUnion<S extends string> =
  S extends `${infer F}${infer R}`
    ? F | StringToUnion<R>
    : never
type Combination<
  S extends string,
  U extends string = '',
  K = S
> = [S] extends [never]
  ? U
  : K extends S
    ? Combination<Exclude<S, K>, U | `${U}${K}`>
    : U
type AllCombinations<S extends string> = Combination<StringToUnion<S>>

代码详解:

  • StringToUnion是用来将字符串变成一个联合类型的,例如:
// 结果: 'A' | 'B'
type result = StringToUnion<'AB'>
  • Combination是用来将联合类型进行排列组合的,以以上'A' | 'B'这个联合类型为例,步骤如下:
// 第一步:从'A' | 'B这个联合类型中排除当前迭代的字符'A'
K = 'A' S = 'A' | 'B'  => Exclude<'A' | 'B', 'A'>
// 第一步子递归:
Combination<'B', '' | 'A'> => '' | 'A' | `${'' | 'A'}B` => '' | 'A' | 'B' | 'AB'

// 第二步:从'A' | 'B'这个联合类型中排除当前迭代的字符'B'
K = 'B' S = 'A' | 'B' => Exclude<'A' | 'B', 'B'>
// 第二步子递归:
Combination<'A', '' | 'B'> => '' | 'B' | `${'' | 'B'}A` => '' | 'B' | 'A' | 'BA'

// 结果:剔除相同元素
result = '' | 'A' | 'AB' | 'B' | 'BA' 

GreaterThan(大于)

用法

GreaterThan<T, N>是来用判断正整数T是否大于正整数N的,其用法如下:

// 结果:true
type result = GreaterThan<2, 1>

实现方式

// 如果比较的数比较大,会提示:Type instantiation is excessively deep and possibly infinite
type GreaterThan<
  T extends Number,
  N extends Number,
  R extends any[] = []
> = T extends R['length']
    ? false
    : N extends R['length']
      ? true
      : GreaterThan<T, N, [...R, 0]>

代码详解:使用一个空数组来辅助,每次递归添加一个元素,如果正整数T先等于这个数组的长度,则为false;如果正整数N先等于这个数组的长度,则为true

Zip(按位置匹配)

用法

Zip是用来将两个元组按照相同索引位置组合成一个新数组的,用法如下:

// 结果:[[1, true], [2, false]]
type result = Zip<[1, 2], [true, false]>

实现方式

type Zip<
  T extends readonly any[],
  U extends readonly any[]
> = T extends [infer First, ...infer Rest]
    ? U extends [infer Head, ...infer Tail]
      ? [[First, Head], ...Zip<Rest, Tail>]
      : []
    : []

IsTuple(是否为元组)

用法

IsTuple是用来判断是否为一个元组的,用法如下:

// 结果:true
type result = IsTuple<[number]>

实现方式

type IsTuple<T> =
  [T] extends [never]
    ? false
    : T extends readonly any[]
      ? number extends T['length']
        ? false
        : true
      : false

代码解析:以上代码中,比较关键的代码是number extends T['length'],这里不能写成T['length'] extends number,如下:

// case1:需要返回false,因为它不定长,违反了元组的定义
type result1 = IsTuple<number[]>
// case2:需要返回true,因为它定长,只不过长度为0
type result2 = IsTuple<[]>

// case1计算逻辑,T['length']返回的是number,不是一个确定的值
number extends T['length']
=> number extends number
=> true

// case2计算逻辑,T['length']返回的是0
number extends T['length']
=> number extends 0
=> false

Chunk(lodash分割数组)

Lodash Chunkopen in new window: 将一个数组分割成长度为N的多个小数组。

用法

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

实现方式

type Chunk<
  T extends any[],
  Size extends number,
  R extends any[] = []
> = R['length'] extends Size
  ? [R, ...Chunk<T, Size>]
  : T extends [infer F, ...infer L]
    ? Chunk<L, Size, [...R, F]>
    : R['length'] extends 0
      ? []
      : [R]

代码详解:实现Chunk大体思路是:借助一个辅助空数组,在遍历数组时往这个辅助数组中添加元素,一直到等于指定长度,然后进行下一次相同操作。

Fill(数组fill方法)

用法

Fill是用来在一个数组中,用指定元素,替换开始索引和结束索引元素的。

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

实现方式

type Fill<
  T extends unknown[],
  N extends number,
  Start extends number = 0,
  End extends number = T['length'],
  Count extends any[] = [],
  Flag extends boolean = Count['length'] extends Start ? true : false
> = Count['length'] extends End
  ? T
  : T extends [infer F, ...infer L]
    ? Flag extends false
      ? [F, ...Fill<L, N, Start, End, [...Count, 0]>]
      : [N, ...Fill<L, N, Start, End, [...Count, 0], true>]
    : T

代码详解:

  • Count: 遍历标志位,从数组第一项开始,当等于End时,结束替换。
  • Flag:遍历标志位,从数组第一项开始,当等于Start是,开始替换。
// 结果:[1, true, true]
type result = Fill<[1, 2, 3], true, 1, 3>

// 第一次遍历 Count = [], Flag = false, T = [1, 2, 3]
// 满足Flag extends false条件,Count = [0]

// 第二次遍历 Count = [0], Flag = true(计算而言),T = [1, 2, 3]
// 不满足Flag extends false条件,开始替换,Count = [0, 0], T = [1, true, 3]

// 第三次遍历 Count = [0, 0], Flag = true(主动传递), T =[1, true, 3]
// 不满足Flag extends false条件,开始替换,Count = [0, 0, 0], T = [1, true, true]

// 最后一次判断 Count = [0, 0, 0],长度等于End,结束,T = [1, true, true]

Without(移除)

用法

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

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

实现方式

type ToUnion<T> = T extends any[] ? T[number] : T
type Without<
  T extends any[],
  F,
  U = ToUnion<F>,
  R extends any[] = []
> = T extends [infer First, ...infer Rest]
    ? First extends U
      ? Without<Rest, F, U, [...R]>
      : Without<Rest, F, U, [...R, First]>
    : R

代码详解:因为F支持单数字和数组,所以定义一个ToUion来统一处理成联合类型。随后直接遍历数组,如果当前迭代的元素在联合类型中,则直接跳过进行下一次迭代;否则,把当前迭代元素添加到R辅助数组中。

Trunc(Math.trunc取整)

用法

Trunc是用来实现Math.trunc()方法的,其用法如下:

// 结果1:100
type result1 = Trunc<100.32>
// 结果2:0
type result2 = Trunc<.3>

实现方式

type Trunc<
  T extends number | string
> =`${T}` extends `${infer L}.${string}`
  ? L extends ''
    ? '0'
    : L
  : `${T}`

IndexOf(数组indexOf方法)

用法

IndexOf是用来实现数组indexOf方法的,其用法如下:

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

实现方式

type IsEqual<X, Y> = 
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

type IndexOf<
  T extends any[],
  U,
  Index extends any[] = []
> = T extends [infer First, ...infer Rest]
  ? IsEqual<U, First> extends true
    ? Index['length']
    : IndexOf<Rest, U, [...Index, 0]>
  : -1

代码详解:需要借助IsEqual来判断两个值是否相等,原因考虑如下案例:

type result1 = IsEqual<1, number> // false
type result2 = IsEqual<'a', string> // false

Join(数组join方法)

用法

Join是用来实现数组join方法的,其用法如下:

// 结果:'a-p-p-l-e'
type result = Join<['a', 'p', 'p', 'l', 'e'], '-'>

实现方式

type Join<
  T extends any[],
  U extends string | number,
  R extends string = ''
> = T extends [infer First, ...infer Rest]
    ? Rest['length'] extends 0
      ? `${R extends '' ? '' : `${R}${U}`}${First&string}`
      : Join<Rest, U, `${R extends '' ? '' : `${R}${U}`}${First&string}`>
    : R

LastIndexOf(数组lastIndexOf方法)

借助IndexOf的实现思路,很容易实现lastIndexOf方法。

用法

LastIndexOf是用来实现数组lastIndexOf方法的,其用法如下:

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

实现方式

type IsEqual<X, Y> = 
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false
type LastIndexOf<
  T extends any[], 
  U
> = T extends [...infer Rest, infer Last]
  ? IsEqual<Last, U> extends true
    ? Rest['length']
    : LastIndexOf<Rest, U>
  : -1

Unique(数组去重)

用法

Unique是用来实现数组去重的,其用法如下:

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

实现方式

借助IsEqualIncludes,很容易实现Unique数组去重。

type IsEqual<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false
type Includes<
  T extends any[],
  U
> = T extends [infer First, ...infer Last]
  ? IsEqual<First, U> extends true
    ? true
    : Includes<Last, U>
  : false

type Unique<
  T extends any[],
  R extends any[] = []
> = T extends [infer First, ...infer Last]
  ? Includes<R, First> extends true
    ? Unique<Last, R>
    : Unique<Last, [...R, First]>
  : R

MapTypes(类型转换)

用法

MapTypes是用来根据指定类型进行替换的,其用法如下:

// 结果:{ type: number; age: number; }
type result = MapTypes<{ type: string; age: number; }, { mapFrom: string;mapTo: number; }>

实现方式

type GetMapType<
  T, 
  R,
  Type = R extends { mapFrom: T, mapTo: infer To } ? To : never
> = [Type] extends [never] ? T : Type
type MapTypes<T, R> = {
  [P in keyof T]: GetMapType<T[P], R>
}

代码详解:在以上的实现中,最核心的代码是获取Type类型。

  • R extends { mapFrom: T, mapTo: infer To }:这段代码表示,R是不是右边的子类型,我们以以上案例来说明:
// 当P = 'type'时,
T[P] = string, R = { mapFrom: string;mapTo: number; }
=> { mapFrom: string; mapTo: number; } extends { mapFrom: string, mapTo: infer To }
=> To = number
=> { type: number }

// 当P = 'age'时
T[P] = number, R = { mapFrom: string;mapTo: number; }
=> { mapFrom: string; mapTo: number; } extends { mapFrom: number, mapTo: infer To }
=> never
=> GetMapType<T[P], R> = number
=> { age: number } 

ConstructTuple(构造元组)

用法

ConstructTuple是用来构造指定长度的元组的,其用法如下:

// 结果:[unknown, unknown]
type result = ConstructTuple<2>

实现方式

type ConstructTuple<
  L extends number,
  R extends any[] = []
> = R['length'] extends L
  ? R
  : ConstructTuple<L, [...R, unknown]>

NumberRange(限定范围数字)

用法

NumberRange是用来返回指定范围内的数字的,其返回的是一个联合类型,用法如下:

// 结果:2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
type result = NumberRange<2, 9>

实现方式

实现思路参考:数组Fill方法。

type NumberRange<
  L, 
  H,
  I extends any[] = [],
  F = I['length'] extends L ? true : false
> = I['length'] extends H
  ? I[number] | H
  : F extends false
    ? NumberRange<L, H, [...I, never]>
    : NumberRange<L, H, [...I, I['length']], true>

Subsequence(元组子序列)

用法

Subsequence是用来根据指定数组生成元组子序列的,其用法如下:

// 结果:[] | [1] | [2] | [1, 2]
type result = Subsequence<[1, 2]>

实现方式

type Subsequence<
  T extends any[],
  R extends any[] = []
> = T extends [infer First, ...infer Last]
  ? Subsequence<Last, R | [...R, First]>
  : R

代码详解:

  • R | [...R, First]: 在一个数组中,对一个联合类型的数组使用...扩展时,会自动进行元素分发,例如:
// 结果:['a', 'b'] | ['a', 'c']
type result = ['a', ...(['b'] | ['c'])]

CheckRepeatedChars(是否包含相同字符)

用法

CheckRepeatedChars是用来检查字符串中是否存在重复字符的,其用法如下:

// 结果1:false 
type result1 = CheckRepeatedChars<'abc'>
// 结果2:true
type result2 = CheckRepeatedChars<'abb'>

实现方式

type CheckRepeatedChars<
  S extends string
> = S extends `${infer First}${infer Last}`
  ? Last extends `${string}${First}${string}`
    ? true
    : CheckRepeatedChars<Last>
  : false

代码详解:

  • ${string}${First}${string}:表示字符串包含First,也可以用infer来代替:${infer Left}${First}${infer Right}

FirstUniqueCharIndex(字符串中第一个唯一字符)

用法

FirstUniqueCharIndex是用来获取字符串中第一个唯一字符的索引的,其用法如下:

// 结果1: 0(字符l)
type result1 = FirstUniqueCharIndex<'leetcode'>
// 结果2: 2(字符v)
type result2 = FirstUniqueCharIndex<'loveleetcode'>

实现方式

借助CheckRepeatedChars的实现思路,很容易实现FirstUniqueCharIndex

type FirstUniqueCharIndex<
  S extends string,
  R extends any[] = []
> = S extends ''
  ? -1
  : S extends `${infer First}${infer Last}`
    ? First extends R[number]
      ? FirstUniqueCharIndex<Last, [...R, First]>
      : Last extends `${string}${First}${string}`
        ? FirstUniqueCharIndex<Last, [...R, First]>
        : R['length']
    : -1

代码详解:此题的实现思路和FirstUniqueCharIndex类似,只是多了一层判断,以上面案例为例:

// 结果: 2(字符v)
type result = FirstUniqueCharIndex<'loveleetcode'>

// 第一次迭代时:S = loveleetcode R = [] R[number] = never First = l
=> 'l' extends never 不满足,'oveleetcode' extends `${string}l${string}`满足

// 第二次迭代时:S = oveleetcode R = ['l'] R[number] = 'l' First = o
=> 'o' extends 'l' 不满足,'veleetcode' extends `${string}o${string}`满足

// 第三次迭代时:S = veleetcode R = ['l', '0'] R[number] = 'l' | 'o' First = v
=> 'v' extends 'l' | 'o' 不满足,'eleetcode' extends `${string}v${string}`不满足

// 结果:R['length']
=> 2

ParseUrlParams(解析url路径参数)

用法

ParseUrlParams是用来解析url上参数名的,其用法如下:

// 结果:'id' | 'user'
type result = ParseUrlParams<'posts/:id/:user'>

实现方式

type ParseUrlParams<
  S extends string
> = S extends `${string}:${infer Last}`
  ? Last extends `${infer Left}/${infer Right}`
    ? Left | ParseUrlParams<Right>
    : Last
  : never

GetMiddleElement(数组中位数)

用法

GetMiddleElement是用来取数组中位数的,其用法如下:

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

实现方式

type GetMiddleElement<
  T extends any[]
> = T['length'] extends 0 | 1 | 2
  ? T
  : T extends [any, ...infer Middle, any]
    ? GetMiddleElement<Middle>
    : never

代码详解:

  • T['length'] extends 0 | 1 | 2:当数组长度小于等于而时,其中位数就是自身。
  • T extends [any, ...infer Middle, any]: 当长度大于2时,每次迭代去掉首、尾元素,直至数组长度小于等于2,返回。

FindOnlyElements(数组只出现一次的元素)

用法

FindOnlyElements是用来获取数组中只出现一次的元素,其用法如下:

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

实现方式

type FindOnlyElements<
  T extends any[],
  U extends any[] = [],
  R extends any[] = []
> = T extends [infer First, ...infer Last]
  ? First extends [...U, ...Last][number]
    ? FindOnlyElements<Last, [...U, First], R>
    : FindOnlyElements<Last, [...U, First], [...R, First]>
  : R

CountArrayElement(计数数组中元素出现的次数)

用法

CountArrayElement是用来实现计算数组中元素出现次数的,其用法如下:

// 结果1: { 1: 1, 2: 1, 3: 1 }
type result1 = CountArrayElement<[1, 2, 3]>
// 结果2: { 1: 2, 2: 2, 3: 1 }
type result2 = CountArrayElement<[1, 2, 2, 1, 3]>

实现方式

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

type ObjectCount<
  T extends any[],
  R extends Record<string | number, any[]> = {}
> = T extends [infer First extends string | number, ...infer Last]
  ? First extends keyof R
    ? ObjectCount<Last, Omit<R, First> & Record<First, [...R[First], 0]>>
    : ObjectCount<Last, Omit<R, First> & Record<First, [0]>>
  : {
    [P in keyof R]: R[P]['length']
  }

type CountArrayElement<
  T extends any[]
> = ObjectCount<Flatten<T>>

代码详解:

  • Flatten:实现Flatten,用来处理传递多维数组的情况,例如:CountArrayElement<[1, [1, 2], 3, [4, [5]]]>
  • First extends keyof R: 如果当前数组的遍历项是R对象中的一个键,则表明需要计数加一;如果不是,则代表是新项,需要计数为1;
  • [P in keyof R]: R[P]['length']: 因为最后结果需要返回数组,而非数组,所以迭代R对象,返回其每个属性的数组长度即可。

Integer(数字整数)

用法

Integer是用来返回数字的整数部分的,如果传入的数子包含小数,则返回never,其用法如下:

// 结果1:1
type result1 = Integer<1>
// 结果2:never
type result1 = Integer<1.1>

实现方式

根据JavaScriptBigIntopen in new window的用法,其不能包含小数。所以实现方式如下:

type Integer<T extends number> = `${T}` extends `${bigint}` ? T : never

代码详解:

  • ${T} extends ${bigint}:这里转成字符串形式比较,不能直接比较,因为numberbigint是两个不同的类型。
// 结果:都是false
type result1 = number extends bigint ? true : false
type result2 = bigint extends number ? true : false

ToPrimitive(转化基本类型)

用法

ToPrimitive是用来返回一个对象的类型的,其用法如下:

type PersonInfo = {
  name: 'Tom'
  age: 30
  married: false
  addr: {
    home: '123456'
    phone: '13111111111'
  }
  hobbies: ['sing', 'dance']
  readonlyArr: readonly ['test']
  fn: () => any
}

type Expected = {
  name: string
  age: number
  married: boolean
  addr: {
    home: string
    phone: string
  }
  hobbies: [string, string]
  readonlyArr: readonly [string]
  fn: Function
}

// 结果:Expected
type result = ToPrimitive<PersonInfo>

实现方式

type ToPrimitive<
  T
> = T extends object
  ? T extends (...args: any[]) => any
    ? Function
    : { [P in keyof T]: ToPrimitive<T[P]> }
  : T extends { valueOf: () => infer R } ? R : T

代码详解:

  • 对于函数来说:当满足T extends (...args: any[]) => any条件时,直接返回Function
  • 对于嵌套对象来说,递归调用ToPrimitive即可。
  • 对于普通类型来说,判断其是否满足T extends { valueOf: () => infer R },是则返回其类型。
// ts中的valueOf是js中的valueOf一样
const num = 123
console.log(num.valueOf()) // 123

DeepMutable(深度Mutable)

用法

DeepMutable是用来深度移除属性readonly修饰符的,其用法如下:

interface Test {
  readonly title: string
  readonly description: string
  readonly completed: boolean
  readonly meta: {
    readonly author: string
  }
}

interface Expected {
  title: string
  description: string
  completed: boolean
  meta: {
    author: string
  }
}

// 结果:Expected
type result = DeepMutable<Test>

实现方式

type DeepMutable<
  T
> = T extends (...args: any[]) => any
  ? T
  : { - readonly [P in keyof T]: DeepMutable<T[P]> }

AllMatch(数组元素是否与给定元素完全相同)

用法

AllMatch是用来判断,数组元素是否与给定元素完全相同的,其用法如下:

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

实现方式

// 不考虑边界情况,简易实现方法
type errTest1 = AllMatch<[any], unknown> // false
type errTest2 = AllMatch<[unknown], any> // false
type errTest3 = AllMatch<[1, 2], 1 | 2>  // false
type AllMatch<
  T extends any[],
  U
> = T[number] extends U
? true
: false;

// 考虑边界情况:完整实现
type IsEqual<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

type AllMatch<
  T extends any[],
  U
> = T extends [infer First, ...infer Rest]
  ? IsEqual<First, U> extends true
    ? AllMatch<Rest, U>
    : false
  : true

Filter(数组过滤)

用法

Filter是用来实现数组过滤方法的,其用法如下:

// 结果1:[2]
type result1 = Filter<[0, 1, 2], 2>
// 结果2:[1, 2]
type result2 = Filter<[0, 1, 2], 1 | 2>

实现方式

type Filter<
  T extends any[],
  P
> = T extends [infer First, ...infer Rest]
  ? First extends P
    ? [First, ...Filter<Rest, P>]
    : Filter<Rest, P>
  : []

FindAllIndex(查找数组中给定元素所有索引)

用法

FindAllIndex是用来返回字符串中所有匹配索引的,其用法如下:

// 结果1:[11]
type result1 = FindAllIndex<'TypeScript type challenges', 'type'>
// 结果2:[2, 13]
type result2 = FindAllIndex<'TypeScript type challenges', 'pe'>
// 结果3:[]
type result3 = FindAllIndex<'TypeScript type challenges', ''>

实现方式

type FindAll<
  T extends string,
  P extends string,
  R extends any[] = [],
  I extends any[] = []
> = P extends ''
  ? []
  : T extends `${string}${infer Last}`
    ? T extends `${P}${string}`
      ? FindAll<Last, P, [...R, I['length']], [...I, 0]>
      : FindAll<Last, P, R, [...I, 0]>
    : R

代码详解:

  • I extends any[] = []: 设置索引,字符串每迭代移除,I数组长度增加一。
  • T extends ${P}${string}: 当满足条件时,向结果数组R中添加当前索引即可。

CombKeys(组合键)

用法

CombKeys是用来实现组合键的,其用法如下:

// 结果:'cmd ctrl' | 'cmd opt' | 'cmd fn' | 'ctrl opt' | 'ctrl fn' | 'opt fn'
type result = CombKeys<['cmd', 'ctrl', 'opt', 'fn']>

实现方式

type CombKeys<
  T extends any[]
> = T extends [infer First extends string, ...infer Last extends string[]]
  ? `${First} ${Last[number]}` | CombKeys<Last>
  : never

ReplaceFirst(替换元组中第一个匹配项)

用法

ReplaceFirst是用来替换元组中第一个匹配项,其用法如下:

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

实现方式

type ReplaceFirst<
  T extends readonly unknown[],
  From,
  To
> = T extends [infer First, ...infer Rest]
  ? First extends From
    ? [To, ...Rest]
    : [First, ...ReplaceFirst<Rest, From, To>]
  : T

困难

SimpleVue(简单Vue类型)

用法

实现方式

Currying(柯里化)

JavaScriptCurrying是用来实现函数柯里化的,其用法如下:

const add = (a: number, b: number) => a + b
const three = add(1, 2)

const curriedAdd = Currying(add)
const five = curriedAdd(2)(3)

用法

Currying是用来实现JavaScript中的柯里化的,其用法如下:

const func = Currying((a: number, b: string, c: boolean) => true)
// 结果:(a: string) => (a: number) => (a: boolean) => true
type funcType = typeof func

实现方式

type CurryFunction<
  P extends any[],
  R
> = P extends []
  ? () => R
  : P extends [infer First, ...infer Rest]
    ? Rest['length'] extends 0
      ? (a: First) => R
      : (a: First) => CurryFunction<Rest, R>
    : R

declare function Currying<F>(fn: F):
  F extends (...args: infer P) => infer R
  ? CurryFunction<P, R>
  : never

代码详解:

  • PP为调用Currying函数时传递函数参数的参数数组,以上面为例,其值为:[number, string, boolean]
  • P extends [infer First, ...infer Rest]: 遍历参数列表,依次返回一个函数即可。

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> = keyof {
  [P in keyof T as ({} extends Pick<T, P> ? never : P)]: P
}

代码详解:

  • {} extends Pick<T, P> ? never : P:是用来判断当前遍历键是否可选键的。
// never
type result = {} extends {} | { sex: undefined } ? never : 'sex'

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 keyof T as (T[P] extends Required<T>[P] ? P : never)]: T[P]
}

代码详解:

  • T[P] extends Required<T>[P] ? P : never:用来判断当前遍历键的类型是否一致,一致则是必填类型。
// P为name时
type result1 = string | undefined extends string ? 'name' : never

// P为age时
type result2 = number extends number ? 'age' : never

OptionalKeys(所有可选字段)

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

用法

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

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

实现方式

type OptionalKeys<T> = keyof {
  [P in keyof T as ({} extends Pick<T, P> ? P : never)]: P
}

代码详解:从上面代码中可以看出,它和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 keyof T as (T[P] extends Required<T>[P] ? never : P)]: 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 First}${infer Rest}`
  ? Uppercase<First> extends Lowercase<First>
    ? `${Capitalize<`${R}${First}`>}${CapitalizeWords<Rest>}`
    : CapitalizeWords<Rest, `${R}${First}`>
  : Capitalize<R>

代码详解:

  • Uppercase<First> extends Lowercase<First>:为了找到连串的大写字符串,例如:
// S = foo bar.hello,world
R = 'foo' First = ' ' Rest = 'bar.hello,world'
=> `${Capitalize<`foo `>}${CapitalizeWords<'bar.hello,world'>}`
=> `Foo ${CapitalizeWords<'bar.hello,world'}`
=> ...
=> 'Foo Bar.Hello,World'

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

用法

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

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

实现方式

type IsLetter<S extends string> = Uppercase<S> extends Lowercase<S> ? false : true
type CamelCase<
  S extends string,
  R extends string = ''
> = S extends `${infer First}${infer Rest}`
  ? CamelCase<
      Rest,
      IsLetter<First> extends true
      ? R extends `${infer P}_`
        ? `${P}${Uppercase<First>}`
        : `${R}${Lowercase<First>}`
      : `${R}${First}`
    >
  : R

代码详解:

  • IsLetter: 用来判断是否为字母的。
type result1 = IsLetter<'$'> // false
type result2 = IsLetter<'A'> // true
  • IsLetter<L> extends true: 如果是字母的话,则根据是否以下划线结尾,如果是,则只需要紧邻下划线的字母L大写即可,否小写。

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'
}

// way1: 借助辅助数组
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

// way2: 不借助辅助数组
type ParsePrintFormat<
  S extends string
> = S extends `${string}%${infer Char}${infer Rest}`
  ? Char extends keyof ControlsMap
    ? [ControlsMap[Char], ...ParsePrintFormat<Rest>]
    : ParsePrintFormat<Rest>
  : []

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

// 第一次迭代
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']

VueBasicProps(Vue的Props类型)

用法

实现方式

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,
  },
  'foo.baz': false
  hello: 'world'
}

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

实现方式

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

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

  • 含有.符号的字符串:对于这种情况,我们先判断.符号左侧部分是否满足为T类型的某个key,如果满足,则递归调用Get;如果不满足,则直接返回never
// S1如果是T的属性键,则返回S1;如果不是,则返回never
Get<T[S1 & keyof T], S2>
// 等价于
S1 extends keyof T ? Get<T[S1], S2> : never
  • 不含有.符号的字符串:对于这种情况,我们只需要判断它是否为T类型中的某个key,如果是,则直接取值;如果不是,则返回never
// K如果是T的属性键,则返回K;如果不是,则返回never
T[K & keyof T]
// 等价于
S extends keyof T ? T[S] : 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'
}
type Expected2 = {
  readonly MacOs: 0;
  readonly Windows: 1;
  readonly Linux: 2
}

// 结果:Expected1
type result1 = TupleToEnum<typeof OperatingSystem>
// 结果:Expected2
type result2 = TupleToEnum<typeof OperatingSystem, true>

实现方式

在实现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[],
  N extends boolean = false
> = {
  readonly [K in TupleKeys<T> as Capitalize<T[K]>]: N extends true ? K : T[K]
}

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

%s表示格式化为(x: string) => any形式,%d表示格式化为(x: number) => any形式。

用法

Format是将字符串格式化为指定函数类型的,用法如下:

// 结果1:(x: string) => string
type result1 = Format<'a%sbc'>
// 结果2:(x: number) => string
type result2 = Format<'a%dbc'>
// 结果3:(x: number) => (x: string) => string>
type result3 = Format<'a%dbc%s'>

实现方式

type FormatMaps = {
  's': string;
  'd': number;
}
type Format<
  S extends string
> = S extends `${infer S1}%${infer P}${infer S2}`
      ? P extends keyof FormatMaps
        ? (x: FormatMaps[P]) => Format<S2>
        : string
      : string

LengthOfString(字符串的长度)

我们之前在中级大章节中已经实现过LengthOfString,但它面临的问题是,如果字符有上百个,由于TS对于递归的次数存在限制,会提示嵌套过深。

用法

// 结果:91
type result = LengthOfString<'1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901'>

实现方式

type LengthOfString<
  S extends string,
  R extends any[] = []
> = S extends `${infer S0}${infer S1}${infer S2}${infer S3}${infer S4}${infer S5}${infer S6}${infer S7}${infer S8}${infer S9}${infer Rest}`
      ? LengthOfString<Rest, [...R, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]>
      : S extends `${infer S1}${infer S2}`
          ? LengthOfString<S2, [...R, S1]>
          : R['length']

代码解析:这里我们巧妙的使用占位的思想,S extends ${infer S1}${infer S2}${infer S3},如果S满足这个占位形式,则表示S的长度至少为2,带入到上面的例子,解析步骤如下:

// 第一次递归
S满足至少10个字符的长度,R = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
// 第二弟递归
S满足至少10个字符的长度,R = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
// 省略
// 最后一次递归
S = '1'不满足至少10个字符的长度,R = [1, ....., 1]
// 最后结果
R['length'] = 91

UnionToTuple(联合类型转元组)

用法

UnionToTuple是用来将联合类型转成元组的,用法如下:

// 结果1:['a', 'b']
type result1 = UnionToTuple<'a'>
// 结果2:['a', 'b']
type result2 = UnionToTuple<'a' | 'b'>
// 结果3:['a', 'b']
type result3 = UnionToTuple<'a' | 'b' | never>

实现方式

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

type LastUnion<U> = UnionToIntersection<
  U extends any
    ? (x: U) => 0
    : never
> extends (x: infer R) => 0
  ? R
  : never

type UnionToTuple<
  T,
  Last = LastUnion<T>
> = [T] extends [never]
  ? []
  : [...UnionToTuple<Exclude<T, Last>>, Last] 

代码详解:

  • UnionToIntersection: 联合类型取交集,在之前已经实现过,这里不再赘述。主要理解以下案例:
type f1 = (x: 1) => 0
type f2 = (x: 2) => 0

// 函数重载结果
// function (x: 1): 0;
// function (x: 2): 0;
type result = UnionToIntersection<f1 | f2>

对于函数参数的交集而言,不是简单的把参数取交集,而是"联合"起来,也就是构造一个新的函数类型,即:函数重载

  • LastUnion: 取联合类型最后的一个元素,如果一个函数存在重载的情况,TS会取最后一个函数签名,例如:
type f1 = (x: 1) => 0
type f2 = (x: 2) => 0

// 结果1:2
type result1 = f1 & f2 extends (x: infer R) => 0 ? R : never
// 结果2:1
type result2 = f2 & f1 extends (x: infer R) => 0 ? R : never

Join(字符串拼接)

用法

Join是用来实现拼接字符串的,用法如下:

// 结果1: ''
const Expected1 = join('-')();
// 结果2: 'a'
const Expected2 = join('-')('a');
// 结果3: 'abc'
const Expected3 = join('')('a', 'b', 'c');
// 结果4: 'a-b-c'
const Expected4 = join('-')('a', 'b', 'c');

实现方式

type Tail<T extends string[]> = T extends [any, ...infer Rest] ? Rest : []
type StringJoin<
  D extends string,
  P extends string[] = []
> = P extends []
    ? ''
    : P extends [infer Only]
      ? Only
      : `${P[0]}${D}${StringJoin<D, Tail<P>>}`

declare function join<D extends string>(delimiter: D): <P extends string[] = []>(...parts: P) => StringJoin<D, P>;

DeepPick(深层次Pick)

用法

DeepPick是用来深层次获取属性值的,用法如下:

type Obj = {
  a: number,
  b: string,
  c:  boolean,
  obj: {
    d: number,
    e: string,
    f:  boolean,
    obj2: {
      g: number,
      h: string,
      i: boolean,
    }
  }
}
// 结果1:Obj
type result1 = DeepPick<Obj, ''>
// 结果2:{ a: number; }
type result2 = DeepPick<Obj, 'a'>
// 结果3:{ a: number; } & { obj: { d: number; } }
type result3 = DeepPick<Obj, 'a', 'obj.d'>

实现方式

在之前,我们实现过根据属性路径取值Get,根据其思路我们很容易实现DeepPick,如下:

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

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

type DeepPick<
  T,
  U extends string
> = UnionToIntersection<
  U extends infer keys ? GetType<T, keys> : never
>

Camelize(对象属性键转小驼峰)

用法

Camelize是用来将对象中的key全部转换为小驼峰的,用法如下:

type Person = {
  some_PROP: string;
  prop: {
    another_prop: string;
  };
  array: [
    { snake_case: string; }
  ]
}
type Expected = {
  someProp: string;
  prop: { 
    anotherProp: string;
  };
  array: [
    { snakeCase: string; }
  ]
}

// 结果:Expected
type result = Camelize<Person>

实现方式

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

type Camelize<T> = {
  [K in keyof T as CamelCase<K>]: 
    T[K] extends [infer R]
      ? [Camelize<R>]
      : T[K] extends Object
        ? Camelize<T[K]>
        : T[K]
}

代码详解:CamelCase的实现可以分为两个部分,第一部分来自于处理属性key转小驼峰的情况,第二部分来自于嵌套对象的情况。

  • 处理属性key:根据之前介绍过的as用法,我们可以在in迭代过程中使用as来进一步加工或者处理属性key,也就是CamelCase的部分。
  • 处理嵌套对象:对于T[P]而言,我们考虑嵌套对象为数组和普通对象的情况,首先判断是否为数组类型,如果是则迭代数组递归调用Camelize;如果是普通对象,则直接调用Camelize;如果都不是,则直接返回T[P]即可。

DropString(移除全部字符)

用法

DropString是用来移除全部字符的,用法如下:

// 结果:'ooar!'
type result = DropString<'foobar!', 'fb'>

实现方式

type StrngToUnion<S extends string> = 
  S extends `${infer S1}${infer S2}`
    ? S1 | StrngToUnion<S2>
    : S

type DropString<
  S extends string,
  R extends string,
  U = StrngToUnion<R>
> = S extends `${infer S1}${infer S2}`
    ? S1 extends U
      ? DropString<S2, R>
      : `${S1}${DropString<S2, R>}`
    : S

代码详解:实现DropString的核心是将指定的字符串转换为联合类型,转换之后只需要迭代字符串,判断当前迭代的字符是不是在联合类型中,如果是则直接丢弃,不是则原样保留。

Split(字符串Split方法)

用法

Split是用来实现字符串split方法的,其用法如下:

// 结果:["Hi!", "How", "are", "you?"]
type result = Split<'Hi! How are you?', ' '>

实现方式

type Split<
  S extends string,
  SEP extends string,
  R extends any[] = []
> = S extends `${infer _}`
      ? S extends `${infer S1}${SEP}${infer S2}`
        ? Split<S2, SEP, [...R, S1]>
        : S extends ''
          ? SEP extends ''
            ? R
            : [...R, S]
          : [...R, S]
      : string[]

ClassPublicKeys(类的公共键)

用法

实现方式

IsRequiredKeys(是否为必填key)

用法

IsRequiredKeys是用来判断是否为必填key的,其用法如下:

type Obj = {
  a: number,
  b?: string
}
// 结果1:true
type result1 = IsRequiredKeys<Obj, 'a'>
// 结果2:false
type result2 = IsRequiredKeys<Obj, 'b'>

实现方式

type IsRequiredKey<T, K extends keyof T> = T extends Record<K, T[K]> ? true : false

根据IsRequiredKey的实现思路,我们可以很容易实现IsOptionalKey,如下:

type IsOptionalKey<T, K extends keyof T> = {} extends { [P in K]: T[P] } ? true : false

// 结果1:false
type result1 = IsOptionalKey<Obj, 'a'>
// 结果2:true
type result2 = IsOptionalKey<Obj, 'b'>

ObjectEntries(对象Object.entries方法)

用法

实现方式

IsPalindrome(是否为回文)

用法

实现方式

MutableKeys(所有可写键)

用法

实现方式

Intersection(交集)

用法

实现方式

BinaryToDecimal(二进制转十进制)

用法

实现方式

ObjectKeyPaths(对象属性键路径)

用法

实现方式

TwoSum(LeetCode两数之和)

用法

实现方式

ValidDate(校验是否为合法日期)

用法

实现方式

Assign(对象Object.assign方法)

用法

实现方式

Maximum(数字中的最大值)

用法

实现方式

DeepCapitalize(深度首字母大写)

用法

实现方式

UnionReplace(联合类型替换)

用法

实现方式

FizzBuzz(Fizz和Buzz输出问题)

用法

实现方式

RLE(运行长度编码)

用法

实现方式

ObjectPathArray(对象键路径数组)

用法

实现方式

SnakeCase(字符串下划线连接)

用法

实现方式

IsNegativeNumber(是否为负数)

用法

实现方式

OptionalUndefined(按需转换为可选属性)

用法

实现方式

地狱

撰写中...

最后更新时间:
贡献者: wangtunan