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。在这里,我们结合使用条件类型和类型推断来提取函数的返回类型。
分解条件类型
T extends (...args: any[]) => infer R: - 条件类型用于检查T是否符合提供的模式。 -(...args: any[]) => infer R使用infer关键字来推断函数的返回类型,并将其赋值给类型变量R。 - 如果T符合这个模式,那么R将包含T的返回类型。? 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 内置工具类型 Pick。Pick 用于从对象类型中选择某些键,并生成一个新的对象类型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
在 MyOmit 中,MyPick<T, Exclude<keyof T, K>> 的作用是从对象类型 T 中选择那些没有被排除的键,构建一个新的对象类型。
组合解释
通过 MyOmit 的定义,可以看到它是结合 MyPick 和 Exclude 来实现的:
-
Exclude<keyof T, K>: - 从对象类型T的所有键中排除K指定的键,得到剩下的键。 -
MyPick<T, Exclude<keyof T, K>>: - 使用MyPick从T中选择剩下的键,构建一个新的对象类型。
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 T为K提供了一个默认值,即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 的定义,可以看到它是结合 Omit、Pick 和 Readonly 来实现的:
Omit<T, K>: - 从对象类型T中排除K指定的键,得到剩余部分的类型。Readonly<Pick<T, K>>: - 从对象类型T中选择K指定的键,并将这些键设为只读。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]> }。
分析每个部分
- 检查对象类型
typescript T extends object ?首先检查T是否是对象类型(包括数组、函数等)。如果T是对象类型,则进入下一层次的检查;否则,返回T本身。 - 检查函数类型
typescript T extends (...args: any[]) => any ? T :接着检查T是否是函数类型(即T是否符合(...args: any[]) => any的模式)。如果T是函数类型,则直接返回T而不做任何处理。 - 递归处理对象属性
typescript { readonly [K in keyof T]: DeepReadonly<T[K]> }如果T不是函数类型,那么就认为T是一个普通对象。对这个对象的每个属性K递归地应用DeepReadonly类型,确保所有嵌套对象的属性也被设为只读。 - 处理非对象类型
typescript : T如果T既不是对象类型也不是函数类型(例如基本类型number,string,boolean等),直接返回T本身。
TupleToUnion
type TupleToUnion<T> = T extends (infer U)[] ? U : never
注释
类型参数
T:泛型参数T表示一个元组类型或数组类型。
工具类型解释
条件类型
TypeScript 中的条件类型类似于 JavaScript 中的三元表达式 condition ? trueExpression : falseExpression。在这里,我们使用条件类型结合类型推断来实现元组到联合类型的转换。
分解条件类型
T extends (infer U)[]: - 条件类型用于检查T是否符合某个模式。 -(infer U)[]使用infer关键字来推断数组元素的类型,并将其赋值给类型变量U。 - 如果T是一个数组类型,例如[number, string, boolean],那么U将分别推断为number、string和boolean。? 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 类型的核心,用于添加新的配置选项。它是一个泛型方法,具有两个泛型参数 K 和 V:
<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>。
这里用到了两个工具类型:
Omit<T, K>: - 从对象类型T中移除键K,确保新添加的键不会与已存在的键冲突(例如,更新已有的键值)。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 和变长元组。
分解条件类型
- 匹配变长元组模式
typescript T extends [...infer _, infer L]- 这一部分用于匹配变长元组,并尝试提取数组T的最后一个元素。 -...infer _:通过infer关键字推断数组前面的所有元素,并把它们赋值给类型变量_。下划线_表示一个占位符,表示我们不关心具体是什么类型。 -infer L:通过infer关键字提取数组的最后一个元素类型,并将其赋值给类型变量L。 - 条件结果
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 和变长元组。
分解条件类型
- 匹配变长元组模式
T extends [...infer R, infer L]- 这一部分用于匹配变长元组,并尝试从数组
T中提取出最后一个元素。 ...infer R:通过infer关键字推断数组前面的所有元素,并将它们赋值给类型变量R。infer L:通过infer关键字提取数组的最后一个元素类型,并将其赋值给类型变量L。
- 这一部分用于匹配变长元组,并尝试从数组
- 条件结果
? 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的值类型。如果输入T是Promise,则提取其解析类型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 }结构。 具体来说,该条件类型的运作流程如下:
- 检查扩展性:它检查
U是否可以扩展为{ type: T }。 - 匹配和返回:
- 如果
U符合{ type: T },那么结果类型就是U本身。 - 否则,结果类型是
never(never表示不可能的类型)。 通过这个条件类型,可以从联合类型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`
- 这是一个条件类型,用于检查字符串是否以空白字符开头。
- 如果是,则移除该空白字符并继续检查剩余部分(这就是递归的部分)。
- 如果不是,则返回原字符串(意味着左侧不再有空白字符)。
详细分解
- 模式匹配和推断:
S extends `${' ' | '\n' | '\t'}${infer R}`- 这一部分使用了模板字面量类型,以及 TypeScript 的条件类型和模式匹配功能。
${' ' | '\n' | '\t'}${infer R}是一种模板字面量类型,它尝试匹配S是否以空格、换行符或制表符开头。- 如果
S符合这种模式,infer R将推断出去掉前缀后的剩余字符串,并将其赋值给R。
- 递归调用:
? TrimLeft<R>- 如果
S以空白字符开头,那么根据推断得到的R再次调用TrimLeft。 - 这是递归的核心,通过递归逐步去除左侧的所有空白字符。
- 如果
- 终止条件:
: 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 关键字用于在条件类型中引入类型变量,并进行类型推断。
分解条件类型
- 匹配模板字面量模式:
S extends `${infer F}${infer R}`
- 这一部分用于匹配字符串类型
S,并尝试将其分解为首字符F和剩余字符R。 ${infer F}${infer R}利用模板字面量类型进行模式匹配:infer F:通过infer关键字推断字符串的第一个字符,并将其赋值给类型变量F。${infer R}:通过infer关键字推断字符串的剩余部分,并将其赋值给类型变量R。
- 条件结果
? `${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
注释
类型别名解释
- 处理空字符串的情况:
From extends '' ? S :
如果 From 是空字符串,则直接返回 S 原字符串,因为替换一个空字符串没有意义。
2. 使用模板字面量匹配和条件类型进行模式匹配和替换:
S extends `${infer Start}${From}${infer End}` ? `${Start}${To}${End}` :
S extends{From}${infer End}``: 这部分用于匹配字符串S。如果S可以分解成Start、From和End三部分,就进行替换。infer Start: 这是From前面的部分。${From}: 这是要被替换的子字符串。infer End: 这是From后面的部分。
- 如果匹配成功,结果是
\{To}${End}`。这表示用To替换From,并拼接Start和End`。
- 如果没有匹配到
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
注释
类型别名解释
- 处理空字符串的情况:
From extends '' ? S :
如果 From 是空字符串,则直接返回原字符串 S,因为替换一个空字符串没有意义。
- 使用模板字面量匹配和条件类型进行模式匹配和替换:
S extends `${infer Start}${From}${infer End}` ? `${Start}${To}${ReplaceAll<End, From, To>}` :
S extends{From}${infer End}``: 这部分用于匹配字符串S。如果S可以分解成Start、From和End三部分,就进行替换。infer Start:From子字符串前面的部分。${From}: 要被替换的子字符串。infer End:From子字符串后面的部分。
- 如果匹配成功,将第一个匹配项替换为
To,然后递归地对剩余部分End继续进行替换操作:`${Start}${To}${ReplaceAll<End, From, To>}`
- 如果没有匹配到
From:
S;
- 如果没有匹配到
From(即S不包含From),则直接返回原字符串S。
AppendArgument
type AppendArgument<Fn, A> = Fn extends (...args: infer Args) => infer ReturnType
? (...args: [...Args, A]) => ReturnType
: never
注释
类型别名解释
- 检查
Fn是否为函数类型:
Fn extends (...args: infer Args) => infer ReturnType
- 使用条件类型检查
Fn是否是一个函数类型。 (...args: infer Args) => infer ReturnType是模式匹配的一部分:infer Args: 用于推断Fn的参数类型列表,并将其赋值给类型变量Args。infer ReturnType: 用于推断Fn的返回类型,并将其赋值给类型变量ReturnType。
- 构造新的函数类型:
? (...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
注释
类型别名解释
- 处理边界情况:
[T] extends [never]
? []
- 如果
T是never,表示没有任何元素,可以返回空的元组[]。
- 生成排列组合:
: 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放在前面,并将剩余元素的排列组合接在后面。
- 防止无限递归的问题:
- 使用泛型默认参数
K = T来保持原始类型,这样我们在递归过程中可以一直知道原始的完整类型。
LengthOfString
type LengthOfString<S extends string, T extends any[] = []> = S extends `${infer First}${infer Rest}`
? LengthOfString<Rest, [First, ...T]>
: T['length']
注释
类型别名解释
- 使用模板字面量匹配字符串:
S extends `${infer First}${infer Rest}`
- 利用模板字面量类型将字符串
S拆分为第一个字符First和剩余部分Rest。 - 如果字符串可以这样拆分,就继续进行递归操作。
- 递归处理剩余部分并记录长度:
? LengthOfString<Rest, [First, ...T]>
- 调用
LengthOfString进行递归处理Rest(剩余部分),同时将First加入元组T中记录。 - 通过
[First, ...T]表示将当前字符First插入到元组T的前面,随着递归调用,元组T会逐渐增大,记录已处理的字符数量。
- 返回元组的长度:
: T['length'];
- 如果字符串无法拆分(即空字符串),则返回元组
T的长度T['length'],这代表字符串的长度。
- 初始情况下元组
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
注释
类型别名解释
- 使用类型约束:
type Flatten<T extends any[]> = ...
- 通过
T extends any[]确保输入类型T必须是一个数组类型,如果输入类型不是数组,将导致类型错误。
- 处理空数组的情况:
T extends []
? []
- 如果
T是空数组[],返回空数组[]。
- 递归处理数组的第一项和剩余部分:
: 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。
- 如果
- 处理没有匹配到元组形式的数组情况:
: 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;
}>
注释
- Flatten1
Flatten1是一个简单的映射类型,它接受一个类型T并返回一个新的类型,该类型具有与T相同的键和值。- 它的作用是将一个可能包含交叉类型的对象扁平化,确保每个属性都是唯一的,并且不存在嵌套结构。
- 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}`
注释
- 使用模板字面量类型
`${T}`
- 将
T转换为字符串类型,无论T是number、string还是bigint。 - 模板字面量类型会将
T自动转换为字符串表示。
- 条件类型判断是否为负数
`${T}` extends `-${infer R}` ? `${R}` : `${T}`
- 使用条件类型判断字符串类型的
T是否匹配'-'前缀模式。 - 如果
T是以'-'开头的字符串(例如'-5'),则提取负号后的部分R,并返回R的字符串表示。 - 如果
T不是负数(例如'5'或'10'),直接返回T的字符串表示。
这种方法确保了对于所有输入类型 number、string 和 bigint 都能正确处理,并返回其绝对值的字符串表示。
StringToUnion
type StringToUnion<T extends string> = T extends `${infer First}${infer Rest}`
? First | StringToUnion<Rest>
: never
注释
- 条件类型和模板字面量类型
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可以被分解为First和Rest,则返回First与递归调用StringToUnion<Rest>结果的联合类型。 - 否则,对于空字符串,返回
never。
- 如果
- 递归处理
- 对于非空字符串,通过递归调用
StringToUnion继续处理剩余的字符串部分,直到字符串为空。 - 每次递归调用,将第一个字符添加到最终的联合类型中。
Merge
type Merge<F, S> = {
[K in keyof F | keyof S]: K extends keyof S ? S[K] : F[K];
}
注释
- 定义映射类型
type Merge<F, S> = {
[K in keyof F | keyof S]: K extends keyof S ? S[K] : F[K];
};
- 定义一个映射类型遍历键名
K。 - 键名
K的范围是keyof F | keyof S,即两个对象类型F和S的所有键名的联合类型。
- 条件类型选择值类型
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
注释
- 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,标记IsFirst为false。 - 如果
First是大写字母:- 并且当前字符是第一个字符(即
IsFirst extends true),则不添加连字符。 - 否则,在其前面添加连字符
-并将其转换为小写,然后递归处理Rest,标记IsFirst为false。
- 并且当前字符是第一个字符(即
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);
}
注释
- 获取所有属性:
- 使用
keyof O | keyof O1获取两个对象的所有属性键。
- 排除公共属性:
- 使用
Exclude<keyof O | keyof O1, keyof O & keyof O1>来排除两个对象中都存在的属性键。keyof O & keyof O1表示两个对象共有的属性,Exclude用于从联合类型中移除这些公共属性。
- 构建差异对象:
- 使用映射类型
[K in ...]遍历差异属性键。 - 值的类型为
(K extends keyof O ? O[K] : never) | (K extends keyof O1 ? O1[K] : never):如果键K是O的属性,则使用O[K],否则使用never。同样地,如果键K是O1的属性,则使用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
注释
- 定义 Falsy 类型
type Falsy = false | 0 | '' | [] | { [K in any]: never } | undefined | null;
Falsy是一个联合类型,包含所有“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;
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
注释
- 条件类型与包裹类型
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
注释
- 辅助类型
IsNevertype IsNever<T> = [T] extends [never] ? true : false;- 使用
[T] extends [never]来检测类型T是否为never。如果T是never,则返回true;否则,返回false。
- 使用
- 主类型
IsUniontype 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。如果T是never,直接返回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]
}
注释
- 遍历类型
U的键- 我们使用映射类型
[K in keyof U]来遍历联合类型U的每个键。
- 我们使用映射类型
- 检查键是否在
T中- 使用条件类型
K extends T来检查当前键是否在需要被替换的键集合T中。
- 使用条件类型
- 替换键的类型
- 如果键在
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]
}
注释
- IsIndexSignature
type IsIndexSignature<K> = string extends K ? true : number extends K ? true : symbol extends K ? true : false;- 这个辅助类型用于检测键
K是否为索引签名。 - 如果
K是可以扩展为string、number或symbol,则认为它是一个索引签名,返回true;否则返回false。
- 这个辅助类型用于检测键
- 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, '']
: ['', '', '']
注释
- 条件类型
A extends{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
注释
-
实现步骤
- 我们需要递归地遍历字符串
S,检查每个字符是否等于C。 - 如果字符等于
C,则不包含它;否则,将其包含在最终结果中。 - 使用 TypeScript 的条件类型和模板字面量类型来实现这一逻辑。
- 我们需要递归地遍历字符串
-
条件类型
S extends{infer Tail}``: 检查字符串S是否可以拆分为一个字符Head和剩余部分Tail。这相当于迭代地遍历字符串的每个字符。Head extends C: 检查当前字符Head是否等于要移除的字符C。- 如果是,则递归调用
DropChar对Tail继续处理(即不包含当前字符Head)。 - 如果不是,将当前字符
Head拼接到结果字符串,并递归调用DropChar对Tail继续处理。
- 如果是,则递归调用
- 如果
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}`>>
注释
核心类型及辅助类型
- NumberLiteral
type NumberLiteral = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';NumberLiteral是一个字符串字面量联合类型,表示单个数字字符。
- MinusOneMap
type MinusOneMap = { '0': '9'; '1': '0'; '2': '1'; '3': '2'; '4': '3'; '5': '4'; '6': '5'; '7': '6'; '8': '7'; '9': '8'; }MinusOneMap是一个映射表,将每个数字字符与其减一后的结果关联起来。
- ReverseString
type ReverseString<S extends string> = S extends `${infer First}${infer Rest}` ? `${ReverseString<Rest>}${First}` : '';ReverseString递归地反转字符串。通过递归模板字面量,将每个字符从字符串中提取出来连接到结果的结尾。
- RemoveLeadingZeros
type RemoveLeadingZeros<S extends string> = S extends '0' ? S : S extends `${'0'}${infer R}` ? RemoveLeadingZeros<R> : S;RemoveLeadingZeros移除字符串前导零。递归地检查字符串,如果以0开始,就继续处理剩下的部分,直到没有前导零为止。
- Initial
type Initial<T extends string> = ReverseString<T> extends `${infer First}${infer Rest}` ? ReverseString<Rest> : T;Initial获取字符串排除最后一个字符的部分。反转字符串后提取第一个字符,再将剩余部分反转回来。
- ParseInt
type ParseInt<T extends string> = RemoveLeadingZeros<T> extends `${infer Digit extends number}` ? Digit : never;ParseInt将字符串解析为数字。先移除前导零,再处理转换。
核心算法
- 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减一,并拼接回剩余部分。
主类型
- MinusOne
type MinusOne<T extends number> = T extends 0 ? -1 : ParseInt<MinusOneForString<`${T}`>>;MinusOne处理输入为数字0的边界情况,直接返回-1。- 对于其他数字,将其转换为字符串格式,使用
MinusOneForString计算减一后的字符串结果,再通过ParseInt将结果转回数字。