TS类型体操练习笔记-Medium(一)

106 阅读23分钟

type-challenges项目的一些练习记录
当做字典来用

MyReturnType

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

注释

类型参数

  • T extends (...args: any[]) => any:
  • 这是泛型参数 T,必须是一个函数类型。
  • (...args: any[]) => any 表示该类型必须是一个接受任意数量和类型参数并返回任意类型值的函数。

条件类型和类型推断

TypeScript 中的条件类型类似于 JavaScript 中的三元表达式 condition ? trueExpression : falseExpression。在这里,我们结合使用条件类型和类型推断来提取函数的返回类型。

分解条件类型
  1. T extends (...args: any[]) => infer R: - 条件类型用于检查 T 是否符合提供的模式。 - (...args: any[]) => infer R 使用 infer 关键字来推断函数的返回类型,并将其赋值给类型变量 R。 - 如果 T 符合这个模式,那么 R 将包含 T 的返回类型。
  2. ? R : never: - 如果条件为真(即 T 符合函数类型并成功推断出返回类型),则返回被推断的返回类型 R。 - 否则返回 never,表示 T 不是一个有效的函数类型。

MyOmit

type MyOmit<T, K extends keyof any> = MyPick<T, Exclude<keyof T, K>>

注释

类型参数

  • T:

    • 泛型参数 T 表示一个对象类型。
  • K extends keyof any:

    • K 表示需要从对象类型 T 中排除的键。
    • keyof any 实际上表示一个字符串类型或数字类型,用以确保 K 的类型是符合键的类型。

工具类型解释

Exclude

Exclude 是 TypeScript 提供的内置工具类型,用于从联合类型中排除某些类型。

type Exclude<T, U> = T extends U ? never : T;

MyOmit 中,Exclude<keyof T, K> 的作用是从 T 的所有键 (keyof T) 中排除 K 指定的键。

MyPick

假设 MyPick 是类似于 TypeScript 内置工具类型 PickPick 用于从对象类型中选择某些键,并生成一个新的对象类型。

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

MyOmit 中,MyPick<T, Exclude<keyof T, K>> 的作用是从对象类型 T 中选择那些没有被排除的键,构建一个新的对象类型。

组合解释

通过 MyOmit 的定义,可以看到它是结合 MyPickExclude 来实现的:

  1. Exclude<keyof T, K>: - 从对象类型 T 的所有键中排除 K 指定的键,得到剩下的键。

  2. MyPick<T, Exclude<keyof T, K>>: - 使用 MyPickT 中选择剩下的键,构建一个新的对象类型。

MyReadonly2

type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> &
  Readonly<Pick<T, K>>

注释

类型参数

  • T:
    • 泛型参数 T 表示一个对象类型。
  • K extends keyof T = keyof T:
    • K 表示需要变为只读的键,它必须是 T 的键之一。
    • = keyof TK 提供了一个默认值,即 T 的所有键。如果 K 未显式指定,那么默认情况下,K 就是 T 的所有键。

工具类型解释

Omit

Omit 是 TypeScript 提供的内置工具类型,用于从对象类型中排除某些键,并生成一个新的对象类型。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

MyReadonly2 中,Omit<T, K> 的作用是从对象类型 T 中排除 K 指定的键,得到剩下的部分。

Pick

Pick 是 TypeScript 提供的内置工具类型,用于从对象类型中选择某些键,并生成一个新的对象类型。

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

MyReadonly2 中,Pick<T, K> 的作用是从对象类型 T 中选择 K 指定的键,构建一个新的对象类型。

Readonly

Readonly 是 TypeScript 提供的内置工具类型,用于将对象类型的所有属性设为只读。

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

MyReadonly2 中,Readonly<Pick<T, K>> 的作用是将 K 指定的键设为只读。

组合解释

通过 MyReadonly2 的定义,可以看到它是结合 OmitPickReadonly 来实现的:

  1. Omit<T, K>: - 从对象类型 T 中排除 K 指定的键,得到剩余部分的类型。
  2. Readonly<Pick<T, K>>: - 从对象类型 T 中选择 K 指定的键,并将这些键设为只读。
  3. Omit<T, K> & Readonly<Pick<T, K>>: - 将排除 K 指定的键后的类型与将 K 指定的键设为只读后的类型进行交叉(组合),生成最终的类型。

DeepReadonly

type DeepReadonly<T> = T extends object
  ? T extends (...args: any[]) => any
    ? T // 不处理函数类型
    : { readonly [K in keyof T]: DeepReadonly<T[K]> } // 对对象类型递归
  : T // 非对象类型保持不变

注释

类型分解

条件类型

TypeScript 中的条件类型类似于 JavaScript 中的三元表达式 condition ? trueExpression : falseExpression。在这里,我们使用条件类型结合类型推断来实现深度只读。

整体结构
  • T extends object ? ... : T:检查 T 是否是对象类型。如果是对象类型,进入下一步处理,否则直接返回 T
  • 内部进一步细分:
    • T extends (...args: any[]) => any ? T : ...:检查 T 是否是函数类型。如果是函数类型,则原样返回 T,不做处理。
    • 否则,对 T 的每个键进行递归处理:{ readonly [K in keyof T]: DeepReadonly<T[K]> }

分析每个部分

  1. 检查对象类型 typescript T extends object ? 首先检查 T 是否是对象类型(包括数组、函数等)。如果 T 是对象类型,则进入下一层次的检查;否则,返回 T 本身。
  2. 检查函数类型 typescript T extends (...args: any[]) => any ? T : 接着检查 T 是否是函数类型(即 T 是否符合 (...args: any[]) => any 的模式)。如果 T 是函数类型,则直接返回 T 而不做任何处理。
  3. 递归处理对象属性 typescript { readonly [K in keyof T]: DeepReadonly<T[K]> } 如果 T 不是函数类型,那么就认为 T 是一个普通对象。对这个对象的每个属性 K 递归地应用 DeepReadonly 类型,确保所有嵌套对象的属性也被设为只读。
  4. 处理非对象类型 typescript : T 如果 T 既不是对象类型也不是函数类型(例如基本类型 number, string, boolean 等),直接返回 T 本身。

TupleToUnion

type TupleToUnion<T> = T extends (infer U)[] ? U : never

注释

类型参数

  • T:泛型参数 T 表示一个元组类型或数组类型。

工具类型解释

条件类型

TypeScript 中的条件类型类似于 JavaScript 中的三元表达式 condition ? trueExpression : falseExpression。在这里,我们使用条件类型结合类型推断来实现元组到联合类型的转换。

分解条件类型
  1. T extends (infer U)[]: - 条件类型用于检查 T 是否符合某个模式。 - (infer U)[] 使用 infer 关键字来推断数组元素的类型,并将其赋值给类型变量 U。 - 如果 T 是一个数组类型,例如 [number, string, boolean],那么 U 将分别推断为 numberstringboolean
  2. ? U : never: - 如果条件为真(即 T 符合数组类型并成功推断出元素类型),则返回被推断的元素类型 U。 - 否则返回 never,表示 T 不是一个有效的数组类型。

Chainable

type Chainable<T = {}> = {
  option: <K extends string, V>(key: K, value: V) => Chainable<Omit<T, K> & Record<K, V>>
  get: () => T
}

注释

类型参数

  • T = {}:泛型参数 T 表示当前的配置对象,默认为空对象 {}。随着链式调用的进行,T 会逐渐累积更多的键值对。

类型成员

option
option: <K extends string, V>(key: K, value: V) => Chainable<Omit<T, K> & Record<K, V>>

这个方法是 Chainable 类型的核心,用于添加新的配置选项。它是一个泛型方法,具有两个泛型参数 KV

  • <K extends string>K 表示配置选项的名称,它必须是字符串类型。
  • <V>V 表示配置选项的值,可以是任意类型。 方法签名解释:
  • (key: K, value: V):方法接受两个参数,一个是键 key,另一个是值 value
  • => Chainable<Omit<T, K> & Record<K, V>>:方法返回一个新的 Chainable 对象,其中新的配置对象类型为 Omit<T, K> & Record<K, V>
这里用到了两个工具类型:
  1. Omit<T, K>: - 从对象类型 T 中移除键 K,确保新添加的键不会与已存在的键冲突(例如,更新已有的键值)。
  2. Record<K, V>: - 创建一个包含键 K 和对应值类型 V 的对象类型。 通过交叉类型 &Omit<T, K>Record<K, V> 合并,得到的新对象类型包含了所有原有的键值对以及新添加的键值对。
get
get: () => T

这个方法用于返回当前的配置对象:

  • () => T:方法不接受任何参数,返回当前的配置对象 T

Last

type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never

注释

类型参数

  • T extends any[]:泛型参数 T 表示一个数组类型。extends any[] 约束 T 必须是一个数组。

工具类型解释

条件类型

TypeScript 中的条件类型类似于 JavaScript 中的三元表达式 condition ? trueExpression : falseExpression。这里我们使用条件类型来实现对数组类型的模式匹配和类型推断。

变长元组类型推断

infer 关键字用于在条件类型中引入类型变量,并进行类型推断。在这里,我们结合使用 infer 和变长元组。

分解条件类型

  1. 匹配变长元组模式 typescript T extends [...infer _, infer L] - 这一部分用于匹配变长元组,并尝试提取数组 T 的最后一个元素。 - ...infer _:通过 infer 关键字推断数组前面的所有元素,并把它们赋值给类型变量 _。下划线 _ 表示一个占位符,表示我们不关心具体是什么类型。 - infer L:通过 infer 关键字提取数组的最后一个元素类型,并将其赋值给类型变量 L
  2. 条件结果 typescript ? L : never - 如果条件为真,即 T 符合变长元组模式,返回类型变量 L(即数组的最后一个元素)。 - 如果条件为假,即 T 不符合变长元组模式,返回 never

Pop

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

注释

类型参数

  • T extends any[]: 泛型参数 T 表示一个数组类型。约束 T 必须是一个数组。

工具类型解释

条件类型

TypeScript 中的条件类型类似于 JavaScript 中的三元表达式 condition ? trueExpression : falseExpression。这里我们使用条件类型来实现对数组类型的模式匹配和类型推断。

变长元组类型推断

infer 关键字用于在条件类型中引入类型变量,并进行类型推断。在这里,我们结合使用 infer 和变长元组。

分解条件类型

  1. 匹配变长元组模式
    T extends [...infer R, infer L]
    
    • 这一部分用于匹配变长元组,并尝试从数组 T 中提取出最后一个元素。
    • ...infer R:通过 infer 关键字推断数组前面的所有元素,并将它们赋值给类型变量 R
    • infer L:通过 infer 关键字提取数组的最后一个元素类型,并将其赋值给类型变量 L
  2. 条件结果
    ? R : []
    
    • 如果条件为真,即 T 符合变长元组模式,返回类型变量 R(即数组去掉最后一个元素后的剩余部分)。
    • 如果条件为假,即 T 不符合变长元组模式,返回空数组类型 []

PromiseAll

declare function PromiseAll<T extends readonly any[]>(values: readonly [...T]): Promise<{
  [K in keyof T]: Awaited<T[K]>
}>

注释

  • Awaited<T>:这是一个辅助类型,它会解析出 Promise 的值类型。如果输入 TPromise,则提取其解析类型 U;否则直接返回 T
  • PromiseAll 函数声明
    • 泛型 T 继承自 readonly any[],表示输入必须是一个只读数组。
    • 在函数参数中使用 readonly [...T],确保函数接收的是一个展开的元组,而不是单纯的数组。
    • 返回类型是 Promise<{ [K in keyof T]: Awaited<T[K]> }>

LookUp

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

注释

这个类型别名 LookUp 是一个条件类型,它用于从联合类型 U 中提取具有特定属性类型 T 的成员。让我们分解一下它的作用:

类型参数

  • U: 一组联合类型或接口。
  • T: 要匹配的属性值。

条件类型

  • U extends { type: T } ? U : never;: 这是一个条件类型,用于检查 U 是否符合 { type: T } 结构。 具体来说,该条件类型的运作流程如下:
  1. 检查扩展性:它检查 U 是否可以扩展为 { type: T }
  2. 匹配和返回
    • 如果 U 符合 { type: T },那么结果类型就是 U 本身。
    • 否则,结果类型是 nevernever 表示不可能的类型)。 通过这个条件类型,可以从联合类型 U 中提取出类型属性为 T 的那一项。 */

// 结合用例 /*

  • 对于 LookUp<Animal, 'dog'>,条件类型会检查联合类型 Animal 中的每一项,看是否符合 { type: 'dog' }。只有 { type: 'dog'; name: string } 符合,因此最终类型是 { type: 'dog'; name: string }
  • 同理对于 LookUp<Animal, 'cat'>LookUp<Animal, 'bird'>,它们分别匹配相应的类型。
  • 对于 LookUp<Animal, 'fish'>,没有任何一项符合 { type: 'fish' },因此结果类型是 never。 综上所述,这个类型别名 LookUp 非常有用,它可以根据特定的类型属性,从联合类型中提取出符合条件的类型。

TrimLeft

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

注释

这个类型别名 TrimLeft 是一个递归条件类型,用于从字符串类型 S 的左侧移除空白字符(空格、换行符或制表符)。

类型参数

  • S extends string: 这是一个泛型参数 S,它必须是一个字符串类型。

条件类型

`S extends `${' ' | '\n' | '\t'}${infer R}` ? TrimLeft<R> : S`
  • 这是一个条件类型,用于检查字符串是否以空白字符开头。
  • 如果是,则移除该空白字符并继续检查剩余部分(这就是递归的部分)。
  • 如果不是,则返回原字符串(意味着左侧不再有空白字符)。

详细分解

  1. 模式匹配和推断
    S extends `${' ' | '\n' | '\t'}${infer R}`
    
    • 这一部分使用了模板字面量类型,以及 TypeScript 的条件类型和模式匹配功能。
    • ${' ' | '\n' | '\t'}${infer R} 是一种模板字面量类型,它尝试匹配 S 是否以空格、换行符或制表符开头。
    • 如果 S 符合这种模式,infer R 将推断出去掉前缀后的剩余字符串,并将其赋值给 R
  2. 递归调用
    ? TrimLeft<R>
    
    • 如果 S 以空白字符开头,那么根据推断得到的 R 再次调用 TrimLeft
    • 这是递归的核心,通过递归逐步去除左侧的所有空白字符。
  3. 终止条件
    : S
    
    • S 不再以空白字符开头时,条件类型的结果将是 S 自身。
    • 这有效地终止了递归

Trim

type TrimRight<S extends string> = S extends `${infer L}${' ' | '\n' | '\t'}` ? TrimRight<L> : S

type Trim<S extends string> = TrimLeft<TrimRight<S>>

注释

TrimRight 同上TrimLeft逻辑。 Trim逻辑上就是先TrimRight再TrimLeft

MyCapitalize

type MyCapitalize<S extends string> = S extends `${infer F}${infer R}` ? `${Uppercase<F>}${R}` : S

注释

类型参数

  • S extends string: 泛型参数 S 表示一个字符串类型。约束 S 必须是一个字符串。

工具类型解释

条件类型

TypeScript 中的条件类型类似于 JavaScript 中的三元表达式 condition ? trueExpression : falseExpression。这里我们使用条件类型来实现对字符串类型的模式匹配和类型操作。

模板字面量类型和类型推断

模板字面量类型允许我们在类型层面上进行字符串拼接和模式匹配。infer 关键字用于在条件类型中引入类型变量,并进行类型推断。

分解条件类型

  1. 匹配模板字面量模式:
S extends `${infer F}${infer R}`
  • 这一部分用于匹配字符串类型 S,并尝试将其分解为首字符 F 和剩余字符 R
  • ${infer F}${infer R} 利用模板字面量类型进行模式匹配:
    • infer F:通过 infer 关键字推断字符串的第一个字符,并将其赋值给类型变量 F
    • ${infer R}:通过 infer 关键字推断字符串的剩余部分,并将其赋值给类型变量 R
  1. 条件结果
? `${Uppercase<F>}${R}` : S
  • 如果条件为真,即字符串 S 匹配模板字面量模式,将首字符 F 转换为大写,返回大写后的首字符与剩余字符拼接的字符串类型。
    • Uppercase<F>:TypeScript 提供的内置工具类型 Uppercase,用于将某个字符转换为大写。
    • ${Uppercase<F>}${R}:将大写后的首字符 F 和剩余字符 R 拼接成新的字符串类型。
  • 如果条件为假,即字符串 S 不匹配模板字面量模式(通常是空字符串的情况),返回原字符串 S

Replace

type Replace<S extends string, From extends string, To extends string> =
  From extends '' ? S :
    S extends `${infer Start}${From}${infer End}` ? `${Start}${To}${End}` :
      S

注释

类型别名解释

  1. 处理空字符串的情况
From extends '' ? S :

如果 From 是空字符串,则直接返回 S 原字符串,因为替换一个空字符串没有意义。 2. 使用模板字面量匹配和条件类型进行模式匹配和替换

S extends `${infer Start}${From}${infer End}` ? `${Start}${To}${End}` :
  • S extends inferStart{infer Start}{From}${infer End}``: 这部分用于匹配字符串 S。如果 S 可以分解成 StartFromEnd 三部分,就进行替换。
    • infer Start: 这是 From 前面的部分。
    • ${From}: 这是要被替换的子字符串。
    • infer End: 这是 From 后面的部分。
  • 如果匹配成功,结果是 \Start{Start}{To}${End}`。这表示用 To替换From,并拼接 StartEnd`。
  1. 如果没有匹配到 From
S;
  • 如果没有匹配到 From(即 S 不包含 From),则直接返回原字符串 S

ReplaceAll

type ReplaceAll<S extends string, From extends string, To extends string> =
  From extends '' ? S :
    S extends `${infer Start}${From}${infer End}` ? `${Start}${To}${ReplaceAll<End, From, To>}` :
      S

注释

类型别名解释

  1. 处理空字符串的情况
From extends '' ? S :

如果 From 是空字符串,则直接返回原字符串 S,因为替换一个空字符串没有意义。

  1. 使用模板字面量匹配和条件类型进行模式匹配和替换
S extends `${infer Start}${From}${infer End}` ? `${Start}${To}${ReplaceAll<End, From, To>}` :
  • S extends inferStart{infer Start}{From}${infer End}``: 这部分用于匹配字符串 S。如果 S 可以分解成 StartFromEnd 三部分,就进行替换。
    • infer Start: From 子字符串前面的部分。
    • ${From}: 要被替换的子字符串。
    • infer End: From 子字符串后面的部分。
  • 如果匹配成功,将第一个匹配项替换为 To,然后递归地对剩余部分 End 继续进行替换操作:
    `${Start}${To}${ReplaceAll<End, From, To>}`
    
  1. 如果没有匹配到 From
S;
  • 如果没有匹配到 From(即 S 不包含 From),则直接返回原字符串 S

AppendArgument

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

注释

类型别名解释

  1. 检查 Fn 是否为函数类型
Fn extends (...args: infer Args) => infer ReturnType
  • 使用条件类型检查 Fn 是否是一个函数类型。
  • (...args: infer Args) => infer ReturnType 是模式匹配的一部分:
    • infer Args: 用于推断 Fn 的参数类型列表,并将其赋值给类型变量 Args
    • infer ReturnType: 用于推断 Fn 的返回类型,并将其赋值给类型变量 ReturnType
  1. 构造新的函数类型
? (...args: [...Args, A]) => ReturnType
  • 如果 Fn 是一个函数类型,将新参数类型 A 添加到原参数类型 Args 的末尾,形成新的参数类型 [..., A]
  • 返回新的函数类型 (...args: [...Args, A]) => ReturnType
  • 否则,返回 never

Permutation

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

注释

类型别名解释

  1. 处理边界情况
[T] extends [never]
  ? []
  • 如果 Tnever,表示没有任何元素,可以返回空的元组 []
  1. 生成排列组合
: T extends T
  ? [T, ...Permutation<Exclude<K, T>>]
  • T extends T 是一种分布式条件类型,依次处理 T 联合类型中的每个元素。
  • 对于每个 T 中的元素,将其与剩下的元素进行组合:
    • [T, ...Permutation<Exclude<K, T>>]
      • Exclude<K, T> 得到除当前元素 T 以外的其他元素。
      • Permutation<Exclude<K, T>> 递归地生成剩余元素的排列组合。
      • [T, ...Permutation<Exclude<K, T>>] 将当前元素 T 放在前面,并将剩余元素的排列组合接在后面。
  1. 防止无限递归的问题
  • 使用泛型默认参数 K = T 来保持原始类型,这样我们在递归过程中可以一直知道原始的完整类型。

LengthOfString

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

注释

类型别名解释

  1. 使用模板字面量匹配字符串
S extends `${infer First}${infer Rest}`
  • 利用模板字面量类型将字符串 S 拆分为第一个字符 First 和剩余部分 Rest
  • 如果字符串可以这样拆分,就继续进行递归操作。
  1. 递归处理剩余部分并记录长度
? LengthOfString<Rest, [First, ...T]>
  • 调用 LengthOfString 进行递归处理 Rest(剩余部分),同时将 First 加入元组 T 中记录。
  • 通过 [First, ...T] 表示将当前字符 First 插入到元组 T 的前面,随着递归调用,元组 T 会逐渐增大,记录已处理的字符数量。
  1. 返回元组的长度
: T['length'];
  • 如果字符串无法拆分(即空字符串),则返回元组 T 的长度 T['length'],这代表字符串的长度。
  1. 初始情况下元组 T 默认值为空数组 []
  • 初始情况下,元组 T 是一个空数组 [],表示尚未处理任何字符。

Flatten

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

注释

类型别名解释

  1. 使用类型约束
type Flatten<T extends any[]> = ...
  • 通过 T extends any[] 确保输入类型 T 必须是一个数组类型,如果输入类型不是数组,将导致类型错误。
  1. 处理空数组的情况
T extends []
  ? []
  • 如果 T 是空数组 [],返回空数组 []
  1. 递归处理数组的第一项和剩余部分
: T extends [infer First, ...infer Rest]
  ? First extends any[]
    ? [...Flatten<First>, ...Flatten<Rest>]
    : [First, ...Flatten<Rest>]
  • 如果 T 是一个非空数组,使用 infer 提取数组的第一项 First 和剩余部分 Rest
  • 检查 First 是否是数组:
    • 如果 First 是数组,递归地展开 First 并将结果与递归展开的 Rest 拼接起来。
    • 如果 First 不是数组,将 First 作为单独的元素,并递归展开 Rest
  1. 处理没有匹配到元组形式的数组情况
: never
  • 这个分支用于处理所有可能未匹配到的情况,如果 T 不符合任何上面的条件,则返回 never

Flatten1

type Flatten1<T> = {
  [K in keyof T]: T[K];
}

type AppendToObject<T, U extends PropertyKey, V> = Flatten1<{
  [K in keyof T]: T[K];
} & {
  [P in U]: V;
}>

注释

  1. Flatten1
    • Flatten1 是一个简单的映射类型,它接受一个类型 T 并返回一个新的类型,该类型具有与 T 相同的键和值。
    • 它的作用是将一个可能包含交叉类型的对象扁平化,确保每个属性都是唯一的,并且不存在嵌套结构。
  2. AppendToObject
    • AppendToObject 类型别名接受三个泛型参数:
      • T:原始对象类型。
      • U:需要添加的属性的键,必须是有效的对象键(PropertyKey)。
      • V:需要添加的属性的值类型。
    • 内部子类型 { [K in keyof T]: T[K]; } 用来遍历和复制原始对象 T 的所有属性。
    • 子类型 { [P in U]: V; } 用来定义新的属性 U 及其对应的值类型 V
    • 使用交叉类型 & 将上述两个子类型合并,这样我们就得到了一个新类型,包含了原始对象 T 的所有属性和新的属性 U: V
    • 最后,我们将这个合并的类型传递给 Flatten1,以确保属性被正确地扁平化,避免交叉类型可能带来的意外行为。

Absolute

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

注释

  1. 使用模板字面量类型
`${T}`
  • T 转换为字符串类型,无论 Tnumberstring 还是 bigint
  • 模板字面量类型会将 T 自动转换为字符串表示。
  1. 条件类型判断是否为负数
`${T}` extends `-${infer R}` ? `${R}` : `${T}`
  • 使用条件类型判断字符串类型的 T 是否匹配 '-' 前缀模式。
  • 如果 T 是以 '-' 开头的字符串(例如 '-5'),则提取负号后的部分 R,并返回 R 的字符串表示。
  • 如果 T 不是负数(例如 '5''10'),直接返回 T 的字符串表示。

这种方法确保了对于所有输入类型 numberstringbigint 都能正确处理,并返回其绝对值的字符串表示。

StringToUnion

type StringToUnion<T extends string> = T extends `${infer First}${infer Rest}`
  ? First | StringToUnion<Rest>
  : never

注释

  1. 条件类型和模板字面量类型
type StringToUnion<T extends string> = T extends `${infer First}${infer Rest}`
  ? First | StringToUnion<Rest>
  : never;
  • T extends ${infer First}${infer Rest}:使用模板字面量类型,匹配字符串 T 的第一个字符和剩余字符。
    • First:表示字符串的第一个字符。
    • Rest:表示字符串剩余的部分。
  • 条件类型 ?
    • 如果 T 可以被分解为 FirstRest,则返回 First 与递归调用 StringToUnion<Rest> 结果的联合类型。
    • 否则,对于空字符串,返回 never
  1. 递归处理
  • 对于非空字符串,通过递归调用 StringToUnion 继续处理剩余的字符串部分,直到字符串为空。
  • 每次递归调用,将第一个字符添加到最终的联合类型中。

Merge

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

注释

  1. 定义映射类型
type Merge<F, S> = {
  [K in keyof F | keyof S]: K extends keyof S ? S[K] : F[K];
};
  • 定义一个映射类型遍历键名 K
  • 键名 K 的范围是 keyof F | keyof S,即两个对象类型 FS 的所有键名的联合类型。
  1. 条件类型选择值类型
K extends keyof S ? S[K] : F[K];
  • 使用条件类型判断 K 是否在 S 中。
  • 如果 K 存在于 S 中,则类型为 S[K],即第二个对象类型中的值。
  • 如果 K 不存在于 S 中,则类型为 F[K],即第一个对象类型中的值。

这种方式确保了如果两个对象都包含相同的键名,新对象类型会采用第二个对象类型中的值。

KebabCase

type KebabCase<S, IsFirst = true> = S extends `${infer First}${infer Rest}`
  ? First extends Lowercase<First>
    ? `${First}${KebabCase<Rest, false>}`
    : `${IsFirst extends true ? '' : '-'}${Lowercase<First>}${KebabCase<Rest, false>}`
  : S

注释

  1. KebabCase 类型
type KebabCase<S, IsFirst = true> = S extends `${infer First}${infer Rest}`
  ? First extends Lowercase<First>
    ? `${First}${KebabCase<Rest, false>}`
    : `${IsFirst extends true ? '' : '-'}${Lowercase<First>}${KebabCase<Rest, false>}`
  : S;
  • 使用 IsFirst 参数来标记当前字符是否为第一个字符,默认为 true
  • 如果 First 是小写字母,则直接保留并递归处理 Rest,标记 IsFirstfalse
  • 如果 First 是大写字母:
    • 并且当前字符是第一个字符(即 IsFirst extends true),则不添加连字符。
    • 否则,在其前面添加连字符 - 并将其转换为小写,然后递归处理 Rest,标记 IsFirstfalse

Diff

type Diff<O, O1> = {
  [K in Exclude<keyof O | keyof O1, keyof O & keyof O1>]: (K extends keyof O ? O[K] : never) | (K extends keyof O1 ? O1[K] : never);
}

注释

  1. 获取所有属性
  • 使用 keyof O | keyof O1 获取两个对象的所有属性键。
  1. 排除公共属性
  • 使用 Exclude<keyof O | keyof O1, keyof O & keyof O1> 来排除两个对象中都存在的属性键。keyof O & keyof O1 表示两个对象共有的属性,Exclude 用于从联合类型中移除这些公共属性。
  1. 构建差异对象
  • 使用映射类型 [K in ...] 遍历差异属性键。
  • 值的类型为 (K extends keyof O ? O[K] : never) | (K extends keyof O1 ? O1[K] : never):如果键 KO 的属性,则使用 O[K],否则使用 never。同样地,如果键 KO1 的属性,则使用 O1[K],否则使用 never。结果是一个联合类型,表示差异属性的类型。

AnyOf

type Falsy = false | 0 | '' | [] | { [K in any]: never } | undefined | null
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
  ? First extends Falsy
    ? AnyOf<Rest>
    : true
  : false

注释

  1. 定义 Falsy 类型
  type Falsy = false | 0 | '' | [] | { [K in any]: never } | undefined | null;
  • Falsy 是一个联合类型,包含所有“falsy”值:false, 0, 空字符串 '', 空数组 [], 空对象 { [K in any]: never }, undefinednull
  1. 判断数组内容
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
  ? First extends Falsy
    ? AnyOf<Rest>
    : true
  : false;
  • T extends [infer First, ...infer Rest]:使用类型推断获取数组的第一个元素 First 和剩余部分 Rest
  • First extends Falsy:如果 First 被认为是“falsy”值,则继续递归检查 Rest
  • 否则,说明有至少一个“truthy”值,直接返回 true
  • 如果数组已经为空(终止条件),直接返回 false

IsNever

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

注释

  1. 条件类型与包裹类型
    type IsNever<T> = [T] extends [never] ? true : false;
    
    • 使用 extends 来检查 T 是否等于 never
    • 我们将 T 包装在元组中 [T] 来避免分配性条件类型(Distributive Conditional Types)的影响,这样 T 将被看作一个整体。
    • 如果 [T] extends [never] 成立,则返回 true,否则返回 false

IsUnion

type IsUnion<T, B = T> = IsNever<T> extends true ? false : T extends any ? [B] extends [T] ? false : true : false

注释

  1. 辅助类型 IsNever
    type IsNever<T> = [T] extends [never] ? true : false;
    
    • 使用 [T] extends [never] 来检测类型 T 是否为 never。如果 Tnever,则返回 true;否则,返回 false
  2. 主类型 IsUnion
    type IsUnion<T, B = T> = IsNever<T> extends true ? false : T extends any ? [B] extends [T] ? false : true : false;
    
    • IsNever<T> extends true ? false: 首先检查 T 是否是 never。如果 Tnever,直接返回 false,因为 never 本身不是联合类型。
    • T extends any: 这是一个条件类型,利用了 TypeScript 的分布式条件类型特性。当 T 是联合类型时,它会被拆解成单独的成员进行判断。
    • [B] extends [T] ? false : true: 在拆解后的每个成员上进行比较。如果 [B] 能够完全扩展到 [T],那么 T 不是联合类型,因此返回 false。否则,返回 true

ReplaceKeys

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

注释

  1. 遍历类型 U 的键
    • 我们使用映射类型 [K in keyof U] 来遍历联合类型 U 的每个键。
  2. 检查键是否在 T
    • 使用条件类型 K extends T 来检查当前键是否在需要被替换的键集合 T 中。
  3. 替换键的类型
    • 如果键在 T 中:
      • 检查该键是否在 Y 中存在相应的新类型(K extends keyof Y)。
      • 如果存在对应的新类型,则使用 Y[K] 作为新的类型。
      • 如果不存在对应的新类型,则将其类型设置为 never
    • 如果键不在 T 中,则保持原来的类型 U[K] 不变。

RemoveIndexSignature

type IsIndexSignature<K> = string extends K ? true : number extends K ? true : symbol extends K ? true : false
type RemoveIndexSignature<T> = {
  [K in keyof T as IsIndexSignature<K> extends true ? never : K]: T[K]
}

注释

  1. IsIndexSignature
    type IsIndexSignature<K> = string extends K ? true : number extends K ? true : symbol extends K ? true : false;
    
    • 这个辅助类型用于检测键 K 是否为索引签名。
    • 如果 K 是可以扩展为 stringnumbersymbol,则认为它是一个索引签名,返回 true;否则返回 false
  2. RemoveIndexSignature
    type RemoveIndexSignature<T> = {
      [K in keyof T as IsIndexSignature<K> extends true ? never : K]: T[K]
    };
    
    • 遍历对象类型 T 的每一个键 K
    • 使用类型映射的条件类型 as 检查 K 是否为索引签名。
      • 如果 K 是索引签名 (IsIndexSignature<K> extends true),将其排除 (never)。
      • 否则,保留这个键 (K)。

PercentageParser

type PercentageParser<A extends string> =
  A extends `${infer Sign}${infer Rest}`
    ? Sign extends '+' | '-'
      ? Rest extends `${infer Number}%`
        ? [Sign, Number, '%']
        : [Sign, Rest, '']
      : A extends `${infer Number}%`
        ? ['', Number, '%']
        : ['', A, '']
    : ['', '', '']

注释

  1. 条件类型
    • A extends inferSign{infer Sign}{infer Rest}: 检查字符串 A是否可以拆分成一个字符Sign和剩余部分Rest`。
    • Sign extends '+' | '-': 检查 Sign 是否是 +-
      • 如果是,则继续检查 Rest
      • Rest extends ${infer Number}%: 检查 Rest是否以%` 结尾。
        • 如果是,则拆分 Number%,返回 [Sign, Number, '%']
        • 如果不是,则返回 [Sign, Rest, '']
    • 如果 Sign 不是 +-,则进一步检查整个字符串:
      • A extends ${infer Number}%: 检查 A是否以%` 结尾。
        • 如果是,则拆分 Number%,返回 ['', Number, '%']
        • 如果不是,则返回 ['', A, '']
    • 如果 A 是空字符串,则返回 ['', '', '']

DropChar

type DropChar<S extends string, C extends string> = S extends `${infer Head}${infer Tail}`
  ? Head extends C
    ? DropChar<Tail, C>
    : `${Head}${DropChar<Tail, C>}`
  : S

注释

  1. 实现步骤

    • 我们需要递归地遍历字符串 S,检查每个字符是否等于 C
    • 如果字符等于 C,则不包含它;否则,将其包含在最终结果中。
    • 使用 TypeScript 的条件类型和模板字面量类型来实现这一逻辑。
  2. 条件类型

    • S extends inferHead{infer Head}{infer Tail}``: 检查字符串 S 是否可以拆分为一个字符 Head 和剩余部分 Tail。这相当于迭代地遍历字符串的每个字符。
    • Head extends C: 检查当前字符 Head 是否等于要移除的字符 C
      • 如果是,则递归调用 DropCharTail 继续处理(即不包含当前字符 Head)。
      • 如果不是,将当前字符 Head 拼接到结果字符串,并递归调用 DropCharTail 继续处理。
    • 如果 S 为空字符串(即递归终止条件),则返回空字符串。

MinusOne

type NumberLiteral = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

// 数字减 1 之后对应的数字
type MinusOneMap = {
  '0': '9'
  '1': '0'
  '2': '1'
  '3': '2'
  '4': '3'
  '5': '4'
  '6': '5'
  '7': '6'
  '8': '7'
  '9': '8'
}

// 字符串相关的工具函数

// 将字符串反转,例如,'abcd' => 'dcba'
type ReverseString<S extends string> = S extends `${infer First}${infer Rest}` ? `${ReverseString<Rest>}${First}` : ''

// 移除字符串开头的 0,例如 '00999' => '999'
type RemoveLeadingZeros<S extends string> = S extends '0' ? S : S extends `${'0'}${infer R}` ? RemoveLeadingZeros<R> : S

// 获取字符串排除最后一个字符的部分,例如 'abcd' => 'abc'
type Initial<T extends string> = ReverseString<T> extends `${infer First}${infer Rest}` ? ReverseString<Rest> : T

// 将字符串解析为数字,例如 '123' => 123
type ParseInt<T extends string> = RemoveLeadingZeros<T> extends `${infer Digit extends number}` ? Digit : never

// 基于一个数字的字符串格式,进行减 1 操作
// 本题的核心算法体现在这里
type MinusOneForString<S extends string> =
  S extends `${Initial<S>}${infer Last extends NumberLiteral}` // 取出最右侧的一位数字
    ? Last extends '0'
      ? `${MinusOneForString<Initial<S>>}${MinusOneMap[Last]}` // 如果是 0,则对当前位减 1 后,还要递归地对左侧的数字进行减 1
      : `${Initial<S>}${MinusOneMap[Last]}` // 如果不是 0,则对当前位减 1 后,直接返回最终结果
    : never

type MinusOne<T extends number> =
  T extends 0 // 处理 T 为 0 这种边界情况,毕竟我们的计算器不支持负数
    ? -1
    : ParseInt<MinusOneForString<`${T}`>>

注释

核心类型及辅助类型

  1. NumberLiteral
    type NumberLiteral = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
    
    • NumberLiteral 是一个字符串字面量联合类型,表示单个数字字符。
  2. MinusOneMap
    type MinusOneMap = {
      '0': '9';
      '1': '0';
      '2': '1';
      '3': '2';
      '4': '3';
      '5': '4';
      '6': '5';
      '7': '6';
      '8': '7';
      '9': '8';
    }
    
    • MinusOneMap 是一个映射表,将每个数字字符与其减一后的结果关联起来。
  3. ReverseString
    type ReverseString<S extends string> = S extends `${infer First}${infer Rest}` ? `${ReverseString<Rest>}${First}` : '';
    
    • ReverseString 递归地反转字符串。通过递归模板字面量,将每个字符从字符串中提取出来连接到结果的结尾。
  4. RemoveLeadingZeros
    type RemoveLeadingZeros<S extends string> = S extends '0' ? S : S extends `${'0'}${infer R}` ? RemoveLeadingZeros<R> : S;
    
    • RemoveLeadingZeros 移除字符串前导零。递归地检查字符串,如果以 0 开始,就继续处理剩下的部分,直到没有前导零为止。
  5. Initial
    type Initial<T extends string> = ReverseString<T> extends `${infer First}${infer Rest}` ? ReverseString<Rest> : T;
    
    • Initial 获取字符串排除最后一个字符的部分。反转字符串后提取第一个字符,再将剩余部分反转回来。
  6. ParseInt
    type ParseInt<T extends string> = RemoveLeadingZeros<T> extends `${infer Digit extends number}` ? Digit : never;
    
    • ParseInt 将字符串解析为数字。先移除前导零,再处理转换。

核心算法

  1. MinusOneForString
    type MinusOneForString<S extends string> =
      S extends `${Initial<S>}${infer Last extends NumberLiteral}`
        ? Last extends '0'
          ? `${MinusOneForString<Initial<S>>}${MinusOneMap[Last]}`
          : `${Initial<S>}${MinusOneMap[Last]}`
        : never;
    
    • MinusOneForString 是核心算法:
      • 首先,分割字符串以提取最后一个字符。
      • 检查最后一个字符是否为 0。如果是,则需要对前面的部分递归减一,然后将最后一位用 9 替换。
      • 如果最后一个字符不是 0,则直接根据 MinusOneMap 减一,并拼接回剩余部分。

主类型

  1. MinusOne
    type MinusOne<T extends number> =
      T extends 0
        ? -1
        : ParseInt<MinusOneForString<`${T}`>>;
    
    • MinusOne 处理输入为数字 0 的边界情况,直接返回 -1
    • 对于其他数字,将其转换为字符串格式,使用 MinusOneForString 计算减一后的字符串结果,再通过 ParseInt 将结果转回数字。