TypeScript 类型体操练习笔记(一)

24 阅读21分钟

进度(41 / 188)

其中标记 ※ 的是我认为比较难或者涉及新知识点的题目

刷题也许没有什么意义,但是喜欢一个人思考一整天的灵光一现,也喜欢看到新奇的答案时的恍然大悟,仅此而已。

Easy

1. Easy - 4 - Pick

从类型 T 中选出符合 K 的属性,构造一个新的类型

type MyPick<T, K extends keyof T> = {
  [key in K]: T[key]
}
  • keyof 是 TypeScript 中的一个关键字,用于获取一个类型的所有属性名组成的联合类型。
  • extends 可以用于约束泛型类型参数、定义类型继承关系和条件类型的判断。
  • 在映射类型中,可以通过 in 关键字遍历联合类型,可以用于遍历一个类型的属性名(需要通过 keyof 获取属性名组成的联合类型),并对每个属性进行相应的操作。

2. Easy - 7 - Readonly

泛型 Readonly<T> 会接收一个泛型参数,并返回一个完全一样的类型,只是所有属性都会是只读 (readonly) 的。

type MyReadonly<T> = {
  readonly [key in keyof T]: T[key];
}
  • 在 TypeScript 中,可以使用 readonly 修饰符来指定一个只读属性。

3. Easy - 11 - Tuple to Object

将一个元组类型转换为对象类型,这个对象类型的键/值和元组中的元素对应。

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
 [key in T[number]]: key
}
  • 表示多种类型的数据,先通过 (string | number | symbol) 表示一个联合,然后 (string | number | symbol)[] 表示联合类型的数组
  • 元组类型是 TypeScript 中的一种特殊数据类型,它允许我们定义一个固定长度和固定类型顺序的数组。
  • 通过 T[number] 可以获取元组 T 所有成员组成的联合类型。
  • 加readonly 的作用,因为测试用例定义的变量都是 constconst的作如下:
const tupleNumber = [1, 2, '33'] // (string | number)[]
const tupleNumber = [1, 2, '33'] as const // readonly [1, 2, "33"]

4. Easy - 14 - First of Array

实现一个 First<T> 泛型,它接受一个数组 T 并返回它的第一个元素的类型。

// 解法一:判断数组长度是否为0
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
// 解法二:判断是否为空数组
type First<T extends any[]> = T extends [] ? never : T[0]
// 解法三:获取T所有元素类型组成的联合类型,如果为never证明为空数组
type First<T extends any[]> = T[number] extends never ? never : T[0];
// 解法四:我们可以通过 keyof T 来获取类型 T 所有键的类型组合,那么如果 '0' 是 T 的键就表示 T 是一个非空数组
type First<T extends any[]> = '0' extends keyof T ? T[0] : never;
// 解法五:infer进行模式匹配
type First<T extends any[]> = T extends [infer A, ...infer _rest] ? A : never
  • 在 TS 中 A extends B ? C : D 的意思是“如果 A 可以赋值给 B,那么类型就是 C,否则就是 D”。
  • T[0] 获取数组 T 下标为 0 的元素类型
  • 在 TS 中,我们通过 extends 和 infer 可以实现一个类似于模式匹配的效果,
  • 其中 ...infer _rest 是我们对元组类型使用的扩展运算,在这里用于表示剩余元素。

5. Easy - 18 - Length of Tuple

创建一个Length泛型,这个泛型接受一个只读的元组,返回这个元组的长度。

type Length<T extends readonly unknown[]> = T['length']
  • T['length'] 是索引访问类型,用于获取对象/数组类型的属性类型
  • 对于元组(tuple),length 是字面量数字类型(如 3)
  • 对于普通数组,length 是 number 类型

6. Easy - 43 - Exclude ※

从联合类型 T 中排除 U 中的类型,来构造一个新的类型。

type MyExclude<T, U> = T extends U ? never : T;
  • T extends U ? ... 中的 T 为联合类型时,会把联合类型中的每一个类型单独进行判断,然后再把结果组合成一个联合类型返回。
  • 如果你想避免这种行为,那么使用 [] 包裹你的类型参数即可,注意在 extends 关键字的两侧都需要: [T] extends [U] ? ...

详解见 《TypeScript 类型体操之 Exclude》

7. Easy - 189 - Awaited

假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。

interface Thenable<T> {
  then: (onfulfilled: (arg: T) => any) => any
}

type MyAwaited<T extends Promise<any> | Thenable<any>> = T extends Promise<infer A> | Thenable<infer A>
  ? A extends Promise<infer _X> | Thenable<infer _X> ? MyAwaited<A> : A
  : never

8. Easy - 268 - IF

实现一个 IF 类型,它接收一个条件类型 C ,一个判断为真时的返回类型 T ,以及一个判断为假时的返回类型 F。 C 只能是 true 或者 false, T 和 F 可以是任意类型。

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

通过 C extends true 来判断 C 是否为 true。

9. Easy - 533 - Concat

在类型系统里实现 JavaScript 内置的 Array.concat 方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。

type Concat<T extends readonly unknown[], U extends readonly unknown[]> = [...T, ...U]
  • 在 TS 中,扩展语法可以对元组类型使用,用法类似于在 JS 中对值的使用

10. Easy - 898 - Includes ※

在类型系统里实现 JavaScript 的 Array.includes 方法,这个类型接受两个参数,返回的类型要么是 true 要么是 false

一开始我想的是 type Includes<T extends readonly any[], U> = U extends T[number] ? true : false

但是 extends 无法保证全等, Expect<Equal<Includes<[{}], { a: 'A' }>, false>> 比如这个 case 会失败。

正确答案为

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
  ? true
  : false;
type Includes<T extends readonly unknown[], U> = 
  T extends [infer F, ...infer Rest]
    ? Equal<F, U> extends true ? true : Includes<Rest, U>
    : false;
  • 其中 [infer F, ...infer Rest] 来提取出元组中的第一个类型,然后通过递归的方式,依次判断每一个类型元素是否和 U 相等。
  • Equal 实现原理很复杂,暂时搞不懂,记住就行

11. Easy - 3057 - Push

type Push<T extends unknown[], U> = [...T, U]

做过上面的 Concat 这道题就很简单了,注意需要通过 T extends unknown[] 来限定 T 的类型,否则无法使用 ...T

12. Easy - 3060 - Unshift

type Unshift<T extends unknown[], U> = [U, ...T]

13. Easy - 3312 - Parameters

实现内置的 Parameters 类型

这种题目一看就是要使用 infer 让 T extends xxx,其中 xxx 里面通过 infer 标记出入参的类型名。不过这里一开始写错了写成了

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

错误的原因:把类型当变量来用了 在做类型体操的时候已经出了很多次错误了。谨记谨记。

正确解法:

type MyParameters<T extends (...args: any[]) => any> = 
    T extends (...args: infer A) => any ? A :never
  • 使用 infer 表示待推断的类型变量,由于 ...args 本身已经是元组类型,因此 infer P 最终推导出的,也是元组类型。

Medium

14. Medium - 2 - Get Return Type 获取函数返回类型

和获取参数类型的逻辑是一模一样的。

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

15. Medium - 3 - 实现 Omit *

解法一,去遍历 T 中除了 K 的键。

type MyExclude<T, P> = T extends P ? never : T;
type MyOmit<T, K extends keyof T> = {
  [P in MyExclude<keyof T, K>]: T[key]
}

解法二,利用 Pick 实现,获取 T 中除了 K 的键

type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

解法三,直接实现

一开始我是这样想的,但是这样不对,因为不应该存在的键也会存在,只不过类型为 never

type MyOmit<T, K extends keyof T> = {
  [P in keyof T]: P extends K ? never : T[key]
}

要将对应的键设置为 never

type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

注意这里的 as 其实是这样的 [P in keyof T as (P extends K ? never : P)]: T[A]as 后面的 P 和前面的命名必须是一样的。

16. Medium - 8 - Readonly 2 对象部分属性只读

MyReadonly2<T, K>TK 包含的属性置为 readonly 其余不变,不传 K 就把所有的属性变成 readonly,此时同 Readonly

重点是 K 可以不传,所以要赋默认值。

从 medium 题目开始,大家可以慢慢地把不同的类型结合起来使用了,一句话写还是比较难。

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

17. Medium - 9 - Deep Readonly 对象属性只读(递归)※

题目要求:实现一个泛型 DeepReadonly<T>,它将对象的每个参数及其子对象递归地设为只读。

type DeepReadonly<T> = {
  readonly [P in keyof T]: keyof T[P] extends never ? T[P] : DeepReadonly<T[P]>;
};

这道题目最难的就是,如何判断一个类型是否需要继续递归只读,而这里最精彩的地方就是关于 never 的应用: keyof T extends never 表示检查 T 是否有可枚举的键。接下来问题就简单多了,如果是存在就递归调用 DeepReadonly 否则直接返回当前类型。

关于 never 的应用其实有很多,在这篇文章中,我曾经总结过一些 《从 Vue3 源码中了解你所不知道的 never》

18. Medium - 10 - Tuple to Union 元组转合集

题目要求:实现泛型 TupleToUnion<T>,它返回元组所有值的合集。

type TupleToUnion<T extends any[]> = T[number];

这类题目单纯考验语法。

  • T[number] 使用了 索引访问类型(Indexed Access Types)
  • 在 TypeScript 中,数组或元组的索引是数字。通过使用 number 关键字作为索引,TypeScript 会获取该数组/元组中 所有 数值索引位置对应的类型,并将它们组合成一个联合类型。

19. Medium - 12 - Chainable Options 可串联构造器 ※

这道题比较复杂,实现一个可以链式调用的串联构造器的类型定义。

type Chainable = {
  option(key: string, value: any): any
  get(): any
}

const result = config
  .option('foo', 123)
  .option('name', 'type-challenges')
  .option('bar', { value: 'Hello World' })
  .get()

// 期望 result 的类型是:
interface Result {
  foo: number
  name: string
  bar: {
    value: string
  }
}

其中 get 比较简单,直接返回当前传入的类型即可。 重点是 option(key: K, value: V),它会接受两个参数,然后返回一个新的 Chainable<X>。X 为 TRecord<K, V> 的联合类型。当前答案:

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

如果不限制 K 的类型,会报错,因为 K 作为对象的键,类型必须为 string | number | symbol,而 keyof any 的结果刚好就是 string | number | symbol,乍一看很神奇,仔细想想也合理:>。

但是在这个case中会报错:

const result3 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 123)
  .get();
  
/*
type Expected3 = {
  name: number;
};
*/

这里要求传入重复的 key 类型时需要报错,同时存在重复的 key 类型时,后面的传入的类型会覆盖前面的类型。

type Chainable<T = {}> = {
  option<K extends keyof any, V>(
    key: K extends keyof T ? never : K,
    value: V
  ): Chainable<Omit<T, K> & Record<K, V>>;
  get(): T;
};

通过指定 K 的类型 K extends keyof T ? never : K 来限制不允许传 T 中已有的属性,在通过 Omit<T, K> 保证 T 中重复属性被覆盖。

20. Medium - 15 - Last of Array 最后一个元素

实现一个 Last<T> 泛型,它接受一个数组 T 并返回其最后一个元素的类型。

这道题不难,但是有一些有趣的解法:

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

这是最直观的解法,[...infer _, infer L] 是 TS 的数组模式匹配。它尝试将数组分为两部分:前面的任意个元素(_)和最后一个元素(L),如果匹配成功,就直接返回 L

type Last<T extends readonly any[]> = T extends [infer Only]
  ? Only
  : T extends [any, ...infer Rest]
  ? Last<Rest>
  : T[0];

尝试通过递归获取最后一个元素,注意一定要先判断数组是否只有一个元素,否则只有一个元素的时候,Rest 会被匹配为 [] 我们就无法拿到最后一个元素了。

type Last<T extends any[]> = [any, ...T][T['length']];

很天才的解法,我想通过 T['length' - 1] 来获取最后一个元素,但是 TS 的类型无法被计算,这种解法是反向思维,在数组的最前面加一个元素,这样 length 对应的就是最后一个类型元素了。

21. Medium - 16 - Pop 排除最后一项

实现一个泛型 Pop<T>,它接受一个数组 T,并返回一个由数组 T 的前 N-1 项(N 为数组 T 的长度)以相同的顺序组成的数组。

type Pop<T extends any[]> = T extends [...infer F, infer _] ? F : [];

这道题目也不难,掌握 infer 的应用即可。因为 Pop<[]> 也是 [] 所以不匹配的时候也为 []

22. Medium - 20 - Promise.all ※

好难的题T^T

type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T 

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

Awaited 部分前面的题目做过就不赘述了。

readonly 表示输入的数组是只读的,[...T] 表示使用了变长元组类型(Variadic Tuple Types) 。它的作用是将传入的参数视为一个元组(Tuple)而不是普通的数组。

[P in keyof T]: Awaited<T[P]> 是一个 映射元组类型(Mapped Tuple Type),它会遍历元组 T 的每一个索引 P,然后把它的类型映射为 Awaited<T[P]>

23. Medium - 62 - Type Lookup 查找类型

通过在联合类型中通过指定公共属性 type 的值来获取相应的类型,例:

interface Cat {
  type: 'cat'
  breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}

interface Dog {
  type: 'dog'
  breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer'
  color: 'brown' | 'white' | 'black'
}

type MyDog = LookUp<Cat | Dog, 'dog'> // expected to be `Dog`

很有趣的题目,因为前面 Exclude 中我们学到了:

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

当 T extends U ? ... 中的 T 为联合类型时,会把联合类型中的每一个类型单独进行判断,然后再把结果组合成一个联合类型返回。

所以一开始我理所当然地写成:

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

但是 TS 语法博大精深,但是只有单独使用联合类型时(naked type),才会使用分布式条件类型,也就是说我们 extends 左边必须是单独的 U

换个思路,我们调整 extends 的右边。所以答案为:

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

24. Medium - 106 - Trim Left 去除左侧空白 ※

实现 TrimLeft<T> ,它接收确定的字符串类型并返回一个新的字符串,其中新返回的字符串删除了原字符串开头的空白字符串。例如:

type trimmed = TrimLeft<'  Hello World  '> // 应推导出 'Hello World  '

这是到目前为止遇到的第一个字符串模板类型的题目。相关知识点可以看下这个文章 《你不知道的 TypeScript:模板字符串类型》

直接看答案吧:

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

在模板字符串类型中,可以通过 infer 实现类似于正则匹配的效果。

首先定义空白字符联合类型 type Space = ' ' | '\n' | '\t'。注意这里 '''\n''\t' 都是 字符串字面量类型,千万不能写成 \s 那不是字符串字面量,而是正则表达式中的语法,TS并没有内置正则表达式引擎。

当联合类型出现在模板字符串插值中时,TS 会尝试匹配联合类型中的任意一个成员。也就是说,它会检查字符串是否以 ' '  '\n'  '\t' 开头。

如果 S 能匹配 ${Space}${infer R} 模式,则 R 为除了第一个空白字符外的剩余字符串组成的类型。这样通过递归我们就能得到 TrimLeft

25. Medium - 108 - Trim 去除两端空白字符

有了上面的题目,这道题就比较简单了,先移除左边,再移除右边空白即可

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

也可以巧妙地使用联合类型,一次性匹配两边的空白:

type Trim<S extends string> =
  S extends `${Space}${infer T}` | `${infer T}${Space}` ? Trim<T> : S;

26. Medium - 110 - Capitalize

将一个字符串类型的首字母转为大写。

type MyCapitalize<S extends string> = 
    S extends `${infer F}${infer X}` ? `${Uppercase<F>}${X}` : S
  • 当使用 ${infer A}${infer B} 这种格式匹配字符串时,TS会采取“非贪婪”策略匹配第一个变量。也就是 A会是第一个字符,B 是剩余所有字符。
  • Uppercase<F> 是 TypeScript 内置的固有类型(Intrinsic Type),将一个字符串类型的全部字符转为大写。

27. Medium - 116 - Replace

实现 Replace<S, From, To> 将字符串 S 中的第一个子字符串 From 替换为 To 。

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

由于测试用例中存在 From 为空串的情况 Equal<Replace<'foobarbar', '', 'foo'>, 'foobarbar'> 此时不需要做任何处理,所以这里先判断了一下,如果 From 为空直接返回原字符串。

然后通过 ${infer A}${From}${infer B} 的模式匹配字符串中出现的第一个 {From} 再通过 ${A}${To}${B} 完成替换。

TS的特性,非贪婪匹配,所以永远会匹配到第一个 {From}

28. Medium - 119 - ReplaceAll

实现 ReplaceAll<S, From, To> 将一个字符串 S 中的所有子字符串 From 替换为 To

一开始就想到了递归处理,实现也就是在上一题稍稍改下。

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

不过很明显这样不对,把修改过的部分再整体替换,会导致下面这种错误。

type Test = ReplaceAll<'foooob', 'ob', 'b'> // "fb"

不过也比较好处理,因为我们知道每次处理的都只会是第一个匹配到的字符串,那么直接递归处理后面的部分就可以了,正确答案:

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

29. Medium - 191 - Append Argument 追加参数

实现一个泛型 AppendArgument<Fn, A>,对于给定的函数类型 Fn,以及一个任意类型 A,返回一个新的函数 GG 拥有 Fn 的所有参数并在末尾追加类型为 A 的参数。

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

这道题就是把之前的获取函数参数和返回值结合起来了,参考 533、3312、2 几道题即可。

30. Medium - 296 - Permutation 联合类型的全排列 ※

实现联合类型的全排列,将联合类型转换成所有可能的全排列数组的联合类型。

题解在此 你不知道的 TypeScript:联合类型与分布式条件类型》,完成此题需要对分布式条件类型有深入理解。

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

31. Medium - 298 - Length of String

计算字符串的长度,类似于 String#length 。

思路:字符串类型没有办法通过 S['length'] 获取长度,但是元组可以,所以最简单的解法就是先把字符串按单个字母分割成元组,然后再取长度。

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

看了社区的答案,可以通过新增一个参数的方式,一次解决问题,虽然不太直观,凡事挺有趣的。

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

32. Medium - 459 - Flatten

你需要写一个接受数组的类型,并且返回扁平化的数组类型。

这道题的思路也是递归,TS中递归的思路就是先处理第一个元素 F,然后递归处理后面的 R。在处理第一个元素的时候,判断是否为数组,是则用 Flatten 递归处理,否则不需要再处理了,把第一个元素的结果和后面的处理结果拼接起来即可。

type Flatten<A extends any[]> =  
A extends [infer F, ...infer R] // 判断A存在第一个元素F
  ? F extends any[] // 判断 F 是否为数组
    ? [...Flatten<F>, ...Flatten<R>] // F为数组则递归处理 F 和剩余部分
    : [F, ...Flatten<R>] // F不为数组则不需要处理 只需要递归处理剩余部分
  : [] // A 不存在第一个元素直接返回空数组

33. Medium - 527 - Append to object

实现一个为接口添加一个新字段的类型。该类型接收三个参数,返回带有新字段的接口类型。

type AppendToObject<T, U extends keyof any, V> = {
  [K in keyof T | U]: K extends keyof T ? T[K] : V
}

这题看着不难,但是还是要注意一点,指定泛型约束 U extends keyof any 因为不是所有类型都能做对象的建。

34. Medium - 529 - Absolute

实现一个接收 string, numberbigInt 类型参数的 Absolute 类型, 返回一个正数字符串。

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

这题很简单,先把 numberbigint 类型转为字符串,再通过字符串模板类型的模式匹配,去掉前面的 -

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

这里涉及到 bigint 的一个知识点

代码末尾的 n 是 JavaScript 和 TypeScript 中 BigInt 数据类型的标志。 数字中间的下划线 _ 是 ES2021 引入的特性(TypeScript 早就支持)。编译器在处理时会完全忽略这些下划线。-1_000_000n 等同于 -1000000n

在模板字符串中引用 bigint 类型。

type A = -1_000_000n; // -1000000n
type S = `${A}`; // "-1000000"

35. Medium - 531 - String to Union

实现一个将接收到的 String 参数转换为一个字母 Union 的类型。

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

这题也属于比较简单,没有新的知识点,每次取首字符然后递归处理。

36. Medium - 599 - Merge

将两个类型合并成一个类型,第二个类型的键会覆盖第一个类型的键。

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

解决难度不大,遍历两个对象的 key,然后优先判断是否为 S 的键,输出 S 对应类型,否则为 F 对应的类型。

37. Medium - 612 - KebabCase

将 驼峰式(camelCase) 或 帕斯卡式(PascalCase) 字符串替换为分号分隔式(kebab-case)。

type DAXIE = 'A'|'B'| 'C'| 'D'| 'E'| 'F'| 'G'| 'H'| 'I'| 'J'| 'K'| 'L'| 'M'| 'N'| 'O'| 'P'| 'Q'| 'R'| 'S'| 'T'| 'U'| 'V'| 'W'| 'X'| 'Y'| 'Z';

type KebabCase<S, X extends string = ''> = 
  S extends `${infer F}${infer R}` // 是否有第一个字母
    ? F extends DAXIE // 存在首字母,判断是否为大写
      ? X extends '' // 看看前面有字符吗
        ? KebabCase<R, `${X}${Lowercase<F>}`> //  前面没有其他字符改成小写但是不加分割线
        : KebabCase<R, `${X}-${Lowercase<F>}`> // 前面有字符 F 处理为 分割线+小写
      : KebabCase<R, `${X}${F}`> // 不是大写字母不做处理
    : X // S处理完了直接返回X

此题有点复杂,但是不涉及新的知识点,还是可以做出来的,我加了完整的注释方便阅读。当然通过枚举的方式来判断是否大写字母有点蠢,肯定有更简单的方式。

type KebabCase<S> = S extends `${infer F}${infer R}`
  ? R extends `${Uncapitalize<R>}` // R首字母为小写或者R为空串时条件为真
    ? `${Uncapitalize<F>}${KebabCase<Uncapitalize<R>>}`
    : `${Uncapitalize<F>}-${KebabCase<Uncapitalize<R>>}`
  : S;

38. Medium - 645 - Diff

获取两个接口类型中的差值属性。

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

虽然是很顺利地写出来了,但是看了别人的答案= =

type Diff<O, O1> = Omit<O & O1, keyof O & keyof O1>

这里比较反直觉的是,对象的交叉类型(Intersection)存在全部对象的属性键。

type A = { name: string };
type B = { age: number };

type C = A & B;
// 结果:{ name: string; age: number; }

这是因为 A 和 B 的交集就是找一个类型,即符合 A 又符合 B。

集合 A:包含所有 “带有 name 属性” 的对象。
集合 B:包含所有 “带有 age 属性” 的对象。
集合 A & B:我要找一个对象,这个对象必须同时拥有 name 和 age。

所以:类型的交集 = 属性名的并集
(TypeATypeB)(KeysAKeysB)(TypeA ∩ TypeB) ⟺ (KeysA ∪ KeysB)

39. Medium - 949 - AnyOf

在类型系统中实现类似于 Python 中 any 函数。类型接收一个数组,如果数组中任一个元素为真,则返回 true,否则返回 false。如果数组为空,返回 false。

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

40. Medium - 1042 - IsNever

实现一个类型 IsNever,它接收输入类型 T。如果该类型解析为 never,则返回 true,否则返回 false

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

这题也简单,主要考察分布式条件类型,当一个联合类型作为泛型应用到条件类型,会有分发效果,而 never 可以被视为空的联合类型,它被应用于条件类型,会直接返回 never,所以需要用 [] 包裹起来,阻止分布式条件类型。

41. Medium - 1097 - IsUnion

实现一个类型 IsUnion,它接受输入类型 T 并返回 T 是否为联合类型。

type IsUnion<T, F = T> = [T] extends [never]
  ? false
  : T extends T
    ? [F] extends [T] ? false : true
  : true;

联合类型特殊的地方就是分布式条件类型,那就利用这个特性:如上代码,如果 T 是条件类型,那么 TF 就是不一样的,否则它俩是一样的类型。

AI总结

通过以上题目,系统地学习了 TypeScript 类型系统的核心知识点:

1. 基础语法

  • keyofextendsin 三大关键字的灵活运用
  • 索引访问类型 T[K]T[number]
  • readonly 修饰符与 as const 断言

2. 条件类型与类型推断

  • 条件类型 A extends B ? C : D 是类型体操的核心
  • infer 关键字实现模式匹配和类型推断
  • 分布式条件类型:联合类型在裸类型参数上的自动分发特性
  • 使用 [T] extends [U] 阻止分布式行为

3. 映射类型

  • [P in keyof T] 遍历对象属性
  • as 关键字重映射键名,可配合条件类型过滤属性
  • 交叉类型 & 合并对象属性

4. 元组与数组

  • 扩展运算符 [...T, U] 在类型层面的应用
  • T[number] 提取元组所有成员的联合类型
  • 元组的 length 是字面量数字类型

5. 字符串模板类型

  • 模板字符串类型支持 infer 进行模式匹配
  • 内置工具:UppercaseLowercaseCapitalizeUncapitalize
  • 联合类型在模板插槽中会匹配任意成员

6. 递归思维

  • 处理数组/字符串:提取首元素 + 递归处理剩余部分
  • 递归终止条件的设计很关键
  • 递归深度有限制,但通常够用

7. 特殊类型特性

  • never 是空联合类型,在条件类型中会直接返回
  • 交叉类型的属性是各类型属性的并集
  • 泛型约束 T extends keyof any 确保类型可作对象键

核心技巧:多数难题都是基础语法的组合应用,关键是理解分布式条件类型和 infer 的工作机制,然后通过递归思维拆解问题。