Code Monkey home page Code Monkey logo

blog-ts-challenges's People

Contributors

astak16 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

jiechen257

blog-ts-challenges's Issues

35. Merge

题目

题目链接:Merge答题

实现 Merge 类型,将两个类型合并为一个新的类型,如果两个类型中有相同的 key,后一个类型的 key 覆盖前面一个类型的 key

import type { Equal, Expect } from "@type-challenges/utils";

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

type cases = [
  Expect<
    Equal<
      Merge<Foo, Bar>,
      {
        a: number;
        b: number;
        c: boolean;
      }
    >
  >
];

答案

方法一

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

方法二

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

方法三

type Merge<T, U> = Omit<Omit<T, keyof U> & U, never>;

14. TupleToUnion

题目

题目链接:TupleToUnion

实现 TupleToUnion ,返回数组的所有项的联合类型。

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<TupleToUnion<[123, "456", true]>, 123 | "456" | true>>,
  Expect<Equal<TupleToUnion<[123]>, 123>>
];

答案

方法一

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

// 等价于

type TupleToUnion<T> = T extends unknown[] ? T[number] : never;

知识点

T[number] 得到数组所有成员的联合类型

方法二

type TupleToUnion<T> = T[number & keyof T];

// 等价于

type TupleToUnion<T> = T[Extract<keyof T, number>];

知识点

  • keyof TT 是数组 keyof T 的结果是索引的联合类型
  • number & keyof T ,筛选出 keyof T 中的 number 类型

TIP

Extract<keyof T, number> = number & keyof T

方法三

type TupleToUnion<T> = T extends Array<infer R> ? R : never;

// 等价于

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

知识点

将数组的中的内容声明为 R

方法四

type TupleToUnion<T> = T extends [infer first, ...infer rest]
  ? first | TupleToUnion<rest>
  : never;

知识点

  • 将数组的第一项声明为 first ,剩余项声明为 rest
  • 如果数组始终有剩余项,将会递归调用 TupleToUnion

方法五

type TupleToUnion<T> = T[any];

知识点

any 可以作为索引类型

方法六

type TupleToUnion<T extends any[]> = keyof {
  [K in T[number]]: K;
};

知识点

将数组变成对象,数组的项作为对象的 key ,使用 keyof 就能得到数组所有项的联合类型。

参考文章

  1. TypeScript: How to get types from arrays

32. AppendToObject

题目

题目链接:AppendToObject答题

实现 AppendToObject,往对象中添加一个属性

import type { Equal, Expect } from "@type-challenges/utils";

type test1 = {
  key: "cat";
  value: "green";
};

type testExpect1 = {
  key: "cat";
  value: "green";
  home: boolean;
};

type test2 = {
  key: "dog" | undefined;
  value: "white";
  sun: true;
};

type testExpect2 = {
  key: "dog" | undefined;
  value: "white";
  sun: true;
  home: 1;
};

type test3 = {
  key: "cow";
  value: "yellow";
  sun: false;
};

type testExpect3 = {
  key: "cow";
  value: "yellow";
  sun: false;
  isMotherRussia: false | undefined;
};

type cases = [
  Expect<Equal<AppendToObject<test1, "home", boolean>, testExpect1>>,
  Expect<Equal<AppendToObject<test2, "home", 1>, testExpect2>>,
  Expect<
    Equal<
      AppendToObject<test3, "isMotherRussia", false | undefined>,
      testExpect3
    >
  >
];

答案

方法一

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

知识点

Tkey 和泛型 K 变成联合类型,然后就可以通过遍历 key 得到新对象

方法二

type IntersectionToObject<T> = { [K in keyof T]: T[K] };
type AppendToObject<T, K extends keyof any, V> = IntersectionToObject<
  T & {
    [key in K]: V;
  }
>;

方法三

type IntersectionToObject<T> = { [K in keyof T]: T[K] };
type AppendToObject<T, K extends keyof any, V> = IntersectionToObject<
  T & Record<K, V>
>;

知识点

方法二和方法三是相同的思路,把两个对象变成交叉对象,然后再将交叉对象转变成普通对象

24. LookUp

题目

题目链接:LookUp

实现 LookUp,满足下面功能

import type { Equal, Expect } from '@type-challenges/utils'

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

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

type Animal = Cat | Dog

type cases = [
  Expect<Equal<LookUp<Animal, 'dog'>, Dog>>,
  Expect<Equal<LookUp<Animal, 'cat'>, Cat>>,
]

答案

方法一

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

T extends { type: U } 会利用分配率

  • Dog extends { type: "dog" } => true => Dog
  • Cat extends { type: "cat" } => false => never

得到 Dog | never 的联合类型,由于 never 是所有类型的子类型,所以最后得到 Dog

方法二

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

简化了方法一,不需要通过对象取值

方法三

type LookUp<T, U extends string> = T extends (U extends T[keyof T] ? T : never)
  ? T
  : never;

方法二中 { type: U },可以写成 U extends T[keyof T] ? T : never

方法四

type LookUp<T extends { type: any }, U extends string> = T extends unknown
  ? T["type"] extends U
    ? T
    : never
  : never;

这里 T["type"] extends U 不会运用分配率,会一直输入 never

为了能够让分配率生效,需要一个前置条件 T extends unknown

34. StringToUnion

题目

题目链接:StringToUnion答题

实现 StringToUnion,接收字符串作为参数,输出该字符串字母的联合类型

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<StringToUnion<"">, never>>,
  Expect<Equal<StringToUnion<"t">, "t">>,
  Expect<Equal<StringToUnion<"hello">, "h" | "e" | "l" | "l" | "o">>,
  Expect<
    Equal<
      StringToUnion<"coronavirus">,
      "c" | "o" | "r" | "o" | "n" | "a" | "v" | "i" | "r" | "u" | "s"
    >
  >
];

答案

将字符串转换成字符的联合类型,有两种方法:

  1. 把字符串打散,用 | 把所有字符连接起来
  2. 把字符串打散,放到元组中,通过 Tuple[number] 把元组变成联合类型

方法一

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

方法二

type StringToUnion<
  T extends string,
  U extends unknown[] = []
> = T extends `${infer F}${infer Rest}`
  ? StringToUnion<Rest, [F, ...U]>
  : U[number];

37. Diff

题目

题目链接:Diff答题

实现 Diff,挑选出两个对象各自独有的属性

import type { Equal, Expect } from "@type-challenges/utils";

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

type cases = [
  Expect<Equal<Diff<Foo, Bar>, { gender: number }>>,
  Expect<Equal<Diff<Bar, Foo>, { gender: number }>>,
  Expect<Equal<Diff<Foo, Coo>, { age: string; gender: number }>>,
  Expect<Equal<Diff<Coo, Foo>, { age: string; gender: number }>>
];

答案

方法一

type Diff<T, U> = Omit<T & U, keyof T & keyof U>;

// 或者这样写
type Diff<T, U> = Omit<T & U, keyof (T | U)>;

知识点

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

type a = keyof Foo & keyof Bar // => "name" | "age"
type b = keyof (Foo | Bar) // => "name" | "age"

方法二

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

// 或者这样写
type Diff<T, U> = {
  [key in Exclude<keyof (T & U), keyof (T | U)>]: (T & U)[key];
};

知识点

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

type a = keyof (Foo & Bar); // "name" | "age" | "gender"
type aa = keyof Foo | keyof Bar; // "name" | "age" | "gender"

type b = keyof (Foo | Bar); // "name" | "age"
type bb = keyof Foo & keyof Bar; // "name" | "age"

type c = Exclude<a, b>; // "gender"

Exclude<...> 有很多种写法:

  • Exclude<keyof (T & U), keyof T & keyof U>
  • Exclude<keyof T | keyof U, keyof T & keyof U>
  • Exclude<keyof T, keyof U> | Exclude<keyof U, keyof T>

方法三

type Diff<T, U> = Required<
  {
    [K in Exclude<keyof T, keyof U>]: T[K];
  } & {
    [K in Exclude<keyof U, keyof T>]: U[K];
  }
>;

// 或者用之前的 Merge 方法
type Diff<T, U> = Merge<
  {
    [K in Exclude<keyof T, keyof U>]: T[K];
  },
  {
    [K in Exclude<keyof U, keyof T>]: U[K];
  }
>;

知识点

  1. T 中排除掉 U 生成一个新的对象
  2. U 中排除掉 T 生成一个新的对象
  3. 合并两个对象

方法四

type Diff<T, U> = Pick<
  T & U,
  Exclude<keyof T, keyof U> | Exclude<keyof U, keyof T>
>;

// 或者这样写
type Diff<
  T,
  U,
  T1 extends keyof T = keyof T,
  U1 extends keyof U = keyof U
> = Pick<
  T & U,
  | { [K in T1]: K extends U1 ? never : K }[T1]
  | { [K in U1]: K extends T1 ? never : K }[U1]
>;

知识点

和方法三思路一样

方法五

type Diff<
  T,
  U,
  T1 extends keyof T = keyof T,
  U1 extends keyof U = keyof U,
  I extends T & U = T & U
> = T extends U
  ? Omit<T, keyof Pick<T, U1>>
  : U extends T
  ? Omit<U, keyof Pick<U, T1>>
  : Required<Omit<T & U, keyof Pick<I, U1>> & Omit<I, keyof Pick<I, T1>>>;

知识点

这是我写的第一个方法,因为一开始没有思路,就想着分三步走

  • T extends U 挑选出 T 中独有的属性
  • U extends T 挑选出 U 中独有的属性
  • TU 没有关系,挑选出 TU 各自独有的属性

3. MyExclude

题目

题目链接:MyExclude

实现一个 MyExclude 满足测试用例 cases

  • 测试用例1:剔除掉联合类型中的 a
  • 测试用例2:剔除掉联合类型中的 "a" | "b"
  • 测试用例3:剔除掉联合类型中的 Function
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<MyExclude<"a" | "b" | "c", "a">, Exclude<"a" | "b" | "c", "a">>>,
  Expect<
    Equal<
      MyExclude<"a" | "b" | "c", "a" | "b">,
      Exclude<"a" | "b" | "c", "a" | "b">
    >
  >,
  Expect<
    Equal<
      MyExclude<string | number | (() => void), Function>,
      Exclude<string | number | (() => void), Function>
    >
  >
];

答案

方法一

type MyExclude<UnionType, ExcludedMembers> = 
  UnionType extends ExcludedMembers ? never : UnionType;

解析

利用分配率,代入运算

type A1 = MyExclude<"a" | "b" | "c", "a">;

/** 代入过程 **/
// ==>
type A1 = MyExclude<"a", "a"> | MyExclude<"b", "a"> | MyExclude<"c", "a">;
// ==>
MyExclude<"a", "a"> // never
MyExclude<"b", "a"> // "b"
MyExclude<"c", "a"> // "c"
// ==>
type A1 = "b" | "c"

这里为什么没有这样定义:type MyExclude<UnionType, ExcludedMembers extends UnionType>,因为如果对 ExcludeMembers 做了限制, () ⇒ voidFunction 就必须匹配,否则无法剔除

方法二

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

解析

K extends T = T 这个语法还不太懂

基础知识

  1. extends 用法

13. DeepReadonly

题目

题目链接:DeepReadonly

实现类型 DeepReadonly ,将对象的每个子对象也变成只读。

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [Expect<Equal<DeepReadonly<X>, Expected>>];

type X = {
  a: () => 22;
  b: string;
  aa: {};
  c: {
    d: boolean;
    e: {
      g: {
        h: { i: true; j: "string" };
        k: "hello";
      };
      l: ["hi", { m: ["hey"] }];
    };
  };
};

type Expected = {
  readonly a: () => 22;
  readonly b: string;
  readonly aa: {};
  readonly c: {
    readonly d: boolean;
    readonly e: {
      readonly g: {
        readonly h: {
          readonly i: true;
          readonly j: "string";
        };
        readonly k: "hello";
      };
      readonly l: readonly ["hi", { readonly m: readonly ["hey"] }];
    };
  };
};

答案

如果是对象,需要递归调用,但函数不需要

方法一

type DeepReadonly<T> = T extends object & { call?: never }
  ? { readonly [key in keyof T]: DeepReadonly<T[key]> }
  : T;

知识点

{call?: never} 是用来排除函数的

方法二

type DeepReadonly<T> = {
  readonly [key in keyof T]: T[key] extends Function
    ? T[key]
    : T[key] extends object
    ? DeepReadonly<T[key]>
    : T[key];
};

知识点

通过 T[key] extends Function 判断是不是函数,不是函数的话在判断是不是 object

方法三

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

// 等价于

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

// 等价于

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

// 等价于

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

知识点

keyof T[key] extends never,如果 T[key] 为函数或者空对象, keyof T[key] 结果为 never

  • keyof () => voidnever
  • keyof {}never

同时 never extends objecttrue

这样就把函数和空对象排除掉了

方法四

type DeepReadonly<T> = T extends Record<string, unknown> | Array<unknown>
  ? { readonly [key in keyof T]: DeepReadonly<T[key]> }
  : T;

知识点

const a: Record<string, unknown> = () => 1; // 报错
const c: Record<string, any> = () => 1; // 不报错

所以 Record<string, unknown> 不能用 any 代替 unknown

18. Enum

题目

题目链接:Tuple to Enum Object回答链接

实现 Enum,满足下面要求

import type { Equal, Expect } from "@type-challenges/utils";

const OperatingSystem = ["macOS", "Windows", "Linux"] as const;
const Command = [
  "echo",
  "grep",
  "sed",
  "awk",
  "cut",
  "uniq",
  "head",
  "tail",
  "xargs",
  "shift",
] as const;

type cases = [
  Expect<Equal<Enum<[]>, {}>>,
  Expect<
    Equal<
      Enum<typeof OperatingSystem>,
      {
        readonly MacOS: "macOS";
        readonly Windows: "Windows";
        readonly Linux: "Linux";
      }
    >
  >,
  Expect<
    Equal<
      Enum<typeof OperatingSystem, true>,
      {
        readonly MacOS: 0;
        readonly Windows: 1;
        readonly Linux: 2;
      }
    >
  >,
  Expect<
    Equal<
      Enum<typeof Command>,
      {
        readonly Echo: "echo";
        readonly Grep: "grep";
        readonly Sed: "sed";
        readonly Awk: "awk";
        readonly Cut: "cut";
        readonly Uniq: "uniq";
        readonly Head: "head";
        readonly Tail: "tail";
        readonly Xargs: "xargs";
        readonly Shift: "shift";
      }
    >
  >,
  Expect<
    Equal<
      Enum<typeof Command, true>,
      {
        readonly Echo: 0;
        readonly Grep: 1;
        readonly Sed: 2;
        readonly Awk: 3;
        readonly Cut: 4;
        readonly Uniq: 5;
        readonly Head: 6;
        readonly Tail: 7;
        readonly Xargs: 8;
        readonly Shift: 9;
      }
    >
  >
];

答案

由题意可知 Enum 接收两个参数

  • 第一个参数是一个 string 数组
  • 第二个参数是 boolean,可选的,默认 false

如果第二个参数没有传递,则输出 keyvalue 都一样的对象

如果第二参数传了 true,则 valuekey 在数组中的索引

方法一

type EnsureArray<T, Type = string> = T extends Type[] ? T : never;

type Enum<
  T extends readonly string[],
  B extends boolean = false,
  R extends Readonly<Record<string, any>> = Readonly<{}>,
  Index extends unknown[] = []
> = T extends readonly [infer F, ...infer Rest]
  ? Enum<
      Readonly<EnsureArray<Rest>>,
      B,
      Readonly<
        R & {
          [K in F & string as Capitalize<K>]: B extends true
            ? Index["length"]
            : K;
        }
      >,
      [...Index, unknown]
    >
  : R;

知识点

第一种方法是递归,把数组的拆成一项一项的,递归调用,生成最终的对象

所以实际的对象会接收 4 个参数,除了第一个参数是必传的,其余的参数都有默认值

  • 第一个参数是个只读 string 数组,必传
  • 第二个参数是个布尔值,默认为 false
  • 第三个参数只读对象,默认为空对象 {}
  • 第四个参数是数组中的索引,默认是空数组 [] ,因为空数组的 length0
  1. 取出数组的第一项

    1. 如果是个空数组输出的会是 RR 默认的是空对象
    T extends [infer F, ...infer Rest] ? ... : R
  2. 如果不是空数组,会进入 true 分支

    1. true 分支就是递归部分

    第一个参数和第二个参数好理解,Rest 数组中剩余的参数,用不用 Readonly 包装都一样,第二个参数就是 boolean

    主要看第三个参数和第四个参数

  3. 第三个参数默认是空对象,但是在递归调用的时候需要把数组项处理成对象

    1. R & {...} 中的 R 第一次是空对象,第二次是第一次 {...} 处理的结果 {readonly MacOS: "macOS"}
    2. {...} 把数组一项变成对象
      1. F & string 是类型约束
      2. Capitalize 把首字母变为大写
      3. 如果第二个参数是 true 则输出 Index["length"] (第一次为 0),否则输出为 K(第一次为 masOS
      4. 如果不用 Readonly 进行包装的话,输出的结果会是 {readonly MasOS: "masOS"} & {...} & {...} 这样的形式,而实际的输出为: {readonly MasOS: "masOS", ... },就需要使用 Readonly 包装,它可以把多个交叉类型的对象合并为一个。
    Readonly<R & {
      [K in F & string as Capitalize<K>]: B extends true
        ? Index["length"]
        : K
    }>,
  4. 第四个参数是个数组,用来计算数组的索引

    • Index["length"] ,第一项为 0,第一项运行结束是变为 [unknown]
    • Index["length"] ,第二项为 2,第二项运行结束是变为 [unknown, unknown]
    • 以此循环直到结束

总结

  1. 交叉类型可以做类型约束: F & string

  2. 把交叉类型合并为一个类型: Readonly<{a: string} & {b: string}> 变为 {readonly a: string; readonly b: string}Required 也可以

  3. 递归中获取索引:

    Arr = []
    Arr["length"]
    [...Arr, unknown]

方法二

type EnsureArray<T, Type = string> = T extends Type[] ? T : never;

type Enum<T extends readonly string[], B extends boolean = false> = {
  readonly [key in T[number] as Capitalize<key>]: B extends true
    ? FindIndex<T, key>
    : key;
};

type FindIndex<
  T extends readonly string[],
  K extends string,
  Index extends unknown[] = []
> = T extends readonly [infer F, ...infer Rest]
  ? F extends K
    ? Index["length"]
    : FindIndex<EnsureArray<Rest>, K, [...Index, unknown]>
  : never;

// FindIndex 另一种写法
type FindIndex<
  T extends readonly string[],
  E extends string,
  Index extends unknown[] = []
> = T extends readonly [infer L, ...infer Rest]
  ? [E, L] extends [L, E]
    ? Index["length"]
    : FindIndex<EnsureArray<Rest>, E, [...Index, unknown]>
  : never;

// FindIndex 另一种写法
type FindIndex<
  T extends readonly string[],
  K extends string,
  Index extends unknown[] = []
> = T[Index["length"]] extends K
  ? Index["length"]
  : FindIndex<T, K, [...Index, unknown]>;

知识点

和方法一的区别是递归只用来获取索引

Enum 接收两个参数:

  • 第一个参数是个只读 string 数组,必传
  • 第二个参数是个布尔值,默认为 false
  1. 遍历,将数组变为对象(如果第二个参数为 false

    type Enum<T extends readonly string[]> = {
      readonly [key in T[number] as Capitalize<key>]: key;
    };
  2. 如果第二个参数为 true,就要计算索引了,计算索引的方法和方法一相同

    FindIndex 接收三个参数:

    • 第一个参数是 Enum 的第一个参数
    • 第二个参数是数组的其中一项
    • 第三个参数是数组中的索引,默认是空数组 [] ,因为空数组的 length0

    这里面取数组的某一项有两种方法:

    • T[Index["length"]]
    • T extends [infer F, ...infer Rest]

    递归调用就能取到索引

总结

  1. 获取元组中某一项有方法:
    • T[Index["length"]]
    • T extends [infer F, ...infer Rest]
  2. [E, L] extends [L, E]
    • [1, 2] extends [2, 1]false 就会递归

方法三

type Enum<T extends readonly string[], B extends boolean = false> = {
  [key in keyof T as T[key] extends string
    ? Capitalize<T[key]>
    : never]: B extends false ? T[key & string] : StringToNumber<key & string>;
};

type StringToNumber<
  S extends string,
  R extends unknown[] = []
> = S extends `${R["length"]}`
  ? R["length"]
  : StringToNumber<S, [...R, unknown]>;

知识点

这里看不太明白的应该是这一段

S extends `${R["length"]}` ? R["length"] : StringToNumber<S, [...R, unknown]>

这里我的理解,输出的应该都是 R["length"],因为传过来的就是字符串。

为了弄明白这一点,我把数字代进去,看结果,首先是 S = "0" 的情况,输出 0 没有问题

// S = "0"
// R["length"] = 0
// 输出 0
"0" extends `0` ? 0 : StringToNumber<S, [...R, unknown]>

S = "1" 的情况,就会循环递归 StringToNumber

// S = "1"
// R["length"] = 0
// 输出 StringToNumber<S, [...R, unknown]>
"1" extends `0` ? 0 : StringToNumber<S, [...R, unknown]>

// S = "1"
// R["length"] = 1
// 1
"1" extends `1` ? 1 : StringToNumber<S, [...R, unknown]>

总结

  1. 为什么 T[number] 可以把元组变成联合类型,因为元组有一个 number 索引

    type Arr = ["a", "b", "c"];
    type A = keyof Arr; // number | "0" | "1" | "2" ...
    type B = Arr[number]; // "a" | "b" | "c"
  2. 递归应用

    // 字符串转数字
    type StringToNumber<
      S extends string,
      R extends unknown[] = []
    > = S extends `${R["length"]}`
      ? R["length"]
      : StringToNumber<S, [...R, unknown]>;
    
    type N = StringToNumber<"2">; // 2
    // 寻找元组下标
    type FindIndex<
      T extends readonly string[],
      K extends string,
      Index extends unknown[] = []
    > = T[Index["length"]] extends K
      ? Index["length"]
      : FindIndex<T, K, [...Index, unknown]>;
    
    type tuple = ["a", "b", "c", "d"];
    type I = FindIndex<tuple, "c">; // 2

方法四

type TupleIndex<T extends readonly unknown[]> = T extends readonly [
  ...infer Rest,
  infer L
]
  ? TupleIndex<Rest> & { [p in L & string]: Rest["length"] }
  : {};

type Enum<T extends readonly string[], N extends boolean = false> = {
  readonly [K in keyof TupleIndex<T> as Capitalize<K & string>]: N extends true
    ? TupleIndex<T>[K]
    : K;
};

知识点

type tuple = ["macOS", "Windows", "Linux"];
type A = keyof TupleIndex<tuple> extends tuple[number] ? true : false; // true

可能你会觉得 keyof TupleIndex<tuple>tuple[number] 是相等的,就会认为上面 keyof TupleIndex<T> 可以替换成 T[number]

type B<T extends string[]> = {
  [K in keyof TupleIndex<T>]: K;
};
type C<T extends string[]> = {
  [K in T[number]]: K;
};

type D = B<tuple> extends C<tuple> ? true : false; // true

但这不能简单的替换,需要做约束

type B<T extends string[]> = {
  [K in keyof TupleIndex<T>]: TupleIndex<T>[K];
};
type C<T extends string[]> = {
  [K in T[number]]: TupleIndex<T>[K]; // error
};
// ==>
type C<T extends string[]> = {
  [K in T[number]]: TupleIndex<T>[K **& keyof TupleIndex<T>**];
};

为什么这里需要做约束呢?

因为 T extends string[]Tstring[] ,所以 Kstring

TupleIndex<T>T 不也是 string[] 么,为什么用 string 访问就会出错呢?

因为经过 TupleIndex<T> 会进行类型推导,会是 T 中的具体类型

也就是说 TupleIndex<T> 最后的类型比 string 更具体

所以直接用 string 去访问就会报错,需要进行约束

方法五

type Format<
  T extends readonly unknown[],
  P extends unknown[] = []
> = T extends readonly [infer F, ...infer R]
  ? [[F, P["length"]], ...Format<R, [...P, unknown]>]
  : [];

type Enum<T extends readonly string[], B extends boolean = false> = {
  readonly [K in Format<T>[number] as Capitalize<K[0]>]: B extends true
    ? K[1]
    : K[0];
};

知识点

Format 实现的结果是双重数组: [["masOS", 0], ["Windows", 1], ["Linus", 2]]

方法六

type TupleIndex<T extends readonly unknown[]> = T extends readonly [
  infer F,
  ...infer R
]
  ? TupleIndex<R> | R["length"]
  : never;

type Enum<T extends readonly string[], B extends boolean = false> = {
  readonly [K in TupleIndex<T> as Capitalize<T[K]>]: B extends true ? K : T[K];
};

知识点

TupleIndex 实现的结果是: TupleIndex<["macOS", "Windows", "Linux"]> // 0 | 2 | 1

总结

  1. A 类型和 B 类型写法上的区别

    A 类型是将所有 Key 的首字母都变为大写,然后再将 tuple 中首字母是小写的那一项的 Key 的首字母变为小写

    B 类型是应用 as 引导一个操作条件 Key 的值没有变

    const tuple = ["macOS", "Windows", "Linux"] as const;
    
    type A<T extends readonly string[]> = {
      [Key in Capitalize<T[number]>]: Key extends T[number]
        ? Key
        : Uncapitalize<Key>;
    };
    
    type B<T extends readonly string[]> = {
      [Key in T[number] as Capitalize<Key & string>]: Key;
    };
    
    type a = A<typeof tuple>;
    type b = B<typeof tuple>;
    type c = a extends b ? true : false; // true
  2. typeof 的用法

    typeof 是用来判断类型的, type b 的结果为什么是 string[],因为 a 的类型是 string[]type c 的类型也就好理解了,因为 tuple 是常量数组。

    const a = ["macOS", "Windows", "Linux"];
    type b = typeof a; // string[]
    
    const tuple = ["macOS", "Windows", "Linux"] as const;
    type c = typeof tuple; // readonly ["macOS", "Windows", "Linux"]
  3. 确定类型

    交叉类型可以做类型约束: F & string => string

    EnsureType<T, C> = T & C;
    EnsureType<T, C> = T extends C ? T : never;
    
    type a = EnsureType<"1", string>; // "1";
    type b = EnsureType<1, string>; // never;
  4. 把交叉类型合并为一个类型: Readonly<{a: string} & {b: string}> 变为 {readonly a: string; readonly b: string}Required 也可以

  5. 获取元组中某一项有方法:

    • T[Index["length"]]
    • T extends [infer F, ...infer Rest]
  6. [E, L] extends [L, E]

    • [1, 2] extends [2, 1]false 就会递归
  7. 为什么 T[number] 可以把元组变成联合类型,因为元组有一个 number 索引

    type Arr = ["a", "b", "c"];
    type A = keyof Arr; // number | "0" | "1" | "2" ...
    type B = Arr[number]; // "a" | "b" | "c"
  8. 递归应用

    Arr = []
    Arr["length"]
    [...Arr, unknown]
    // 字符串转数字
    type StringToNumber<
      S extends string,
      R extends unknown[] = []
    > = S extends `${R["length"]}`
      ? R["length"]
      : StringToNumber<S, [...R, unknown]>;
    
    type N = StringToNumber<"2">; // 2
    // 寻找元组下标
    type FindIndex<
      T extends readonly string[],
      K extends string,
      Index extends unknown[] = []
    > = T[Index["length"]] extends K
      ? Index["length"]
      : FindIndex<T, K, [...Index, unknown]>;
    
    type tuple = ["a", "b", "c", "d"];
    type I = FindIndex<tuple, "c">; // 2

36. KebabCase

题目

题目链接:KebabCase答题

实现 KebabCase,满足下面需求

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<KebabCase<"FooBarBaz">, "foo-bar-baz">>,
  Expect<Equal<KebabCase<"fooBarBaz">, "foo-bar-baz">>,
  Expect<Equal<KebabCase<"foo-bar">, "foo-bar">>,
  Expect<Equal<KebabCase<"foo_bar">, "foo_bar">>,
  Expect<Equal<KebabCase<"Foo-Bar">, "foo--bar">>,
  Expect<Equal<KebabCase<"ABC">, "a-b-c">>,
  Expect<Equal<KebabCase<"-">, "-">>,
  Expect<Equal<KebabCase<"">, "">>,
  Expect<Equal<KebabCase<"😎">, "😎">>
];

答案

方法一

type KebabCase<T> = T extends `${infer FirstLetter}${infer RestLetter}`
  ? Rest extends Uncapitalize<Rest>
    ? `${Uncapitalize<FirstLetter>}${KebabCase<RestLetter>}`
    : `${Uncapitalize<FirstLetter>}-${KebabCase<RestLetter>}`
  : T;

知识点

  1. 把字符串拆分成两部分:FirstLetterRestLetter
  2. 这里不在是判断 FirstLetter 部分,而是判断 RestLetter 部分首字母是否大写
  • 看题意,需要把大写字母变成小写,并且在它前面增加 -
  • 如果判断 FirstLetter,那单词首字母大写的话,就会在单词前面添加 - 不符合题意

22. Pop、Shift、Unshift、Push、Reverse

题目

题目链接:Pop回答链接

实现 Pop,满足下面功能

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<Pop<[3, 2, 1]>, [3, 2]>>,
  Expect<Equal<Pop<["a", "b", "c", "d"]>, ["a", "b", "c"]>>
];

答案

方法一

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

方法二

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

方法三

type Pop<T extends unknown[]> = T extends [infer F, ...infer R]
  ? T["length"] extends 0 | 1
    ? []
    : [F, ...Pop<R>]
  : [];

方法四

type RemoveFirst<T extends any[]> = T extends [infer F, ...infer R] ? R : [];
type Pop<T extends unknown[]> = T["length"] extends 0 | 1
  ? []
  : [T[0], ...Pop<RemoveFirst<T>>];

Tips

Shift

type Shift<T extends unknown[]> = T extends [infer F, ...infer R] ? R : [];

type Shift<T extends unknown[]> = T extends [...infer R, infer L]
  ? R extends []
    ? []
    : [...Shift<R>, L]
  : [];

type Shift<T extends unknown[]> = T extends [...infer R, infer L]
  ? T["length"] extends 0 | 1
    ? []
    : [...Shift<R>, L]
  : [];

type RemoveLast<T extends unknown[]> = T extends [...infer R, infer L] ? R : [];
type Shift<T extends unknown[]> = T["length"] extends 0 | 1
  ? []
  : [...Shift<RemoveLast<T>>, [unknown, ...T][T["length"]]];

Unshift

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

Push

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

Reverse

type Reverse<T extends unknown[], Target extends unknown[] = []> = T extends [
  infer F,
  ...infer R
]
  ? Reverse<R, [F, ...Target]>
  : Target;

16. UnionToTuple

题目

题目链接:UnionToTuple

实现 UnionToTuple ,将联合类型转为元组。

import type { Equal, Expect } from "@type-challenges/utils";

type ExtractValuesOfTuple<T extends any[]> = T[keyof T & number];

type cases = [
  Expect<Equal<UnionToTuple<"a" | "b">["length"], 2>>,
  Expect<Equal<ExtractValuesOfTuple<UnionToTuple<"a" | "b">>, "a" | "b">>,
  Expect<Equal<ExtractValuesOfTuple<UnionToTuple<"a">>, "a">>,
  Expect<Equal<ExtractValuesOfTuple<UnionToTuple<any>>, any>>,
  Expect<
    Equal<ExtractValuesOfTuple<UnionToTuple<undefined | void | 1>>, void | 1>
  >,
  Expect<Equal<ExtractValuesOfTuple<UnionToTuple<any | 1>>, any | 1>>,
  Expect<Equal<ExtractValuesOfTuple<UnionToTuple<any | 1>>, any>>,
  Expect<
    Equal<
      ExtractValuesOfTuple<UnionToTuple<"d" | "f" | 1 | never>>,
      "f" | "d" | 1
    >
  >,
  Expect<
    Equal<ExtractValuesOfTuple<UnionToTuple<[{ a: 1 }] | 1>>, [{ a: 1 }] | 1>
  >,
  Expect<Equal<ExtractValuesOfTuple<UnionToTuple<never>>, never>>,
  Expect<
    Equal<
      ExtractValuesOfTuple<
        UnionToTuple<"a" | "b" | "c" | 1 | 2 | "d" | "e" | "f" | "g">
      >,
      "f" | "e" | 1 | 2 | "g" | "c" | "d" | "a" | "b"
    >
  >
];

答案

这题有两个核心点:

  1. 联合类型转为函数交叉类型

    type Union = 1 | 2;
    type A = (() => 1) & (() => 2);
    // 或
    type A = ((arg: 1) => 0) & ((arg: 2) => 0));
  2. 取出函数交叉类型的最后一个值

    type A = (() => 1) & (() => 2) extends () => infer I ? I : never;
    // 或
    type A = ((arg: 1) => 0) & ((arg: 2) => 0)) extends (arg: infer I) => 0
      ? I : never;

方法一

type UnionToIntersectionFn<U> = (
  U extends unknown ? (k: () => U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

type GetUnionLast<U> = UnionToIntersectionFn<U> extends () => infer I
  ? I
  : never;

type Prepend<Tuple extends unknown[], First> = [First, ...Tuple];

type UnionToTuple<
  Union,
  T extends unknown[] = [],
  Last = GetUnionLast<Union>
> = [Union] extends [never]
  ? T
  : UnionToTuple<Exclude<Union, Last>, Prepend<T, Last>>;

参考文章:TS 类型转换:union(联合)转 tuple(元组)

知识点

第一步:实现 UnionToIntersectionFn 类型

ts 没有提供从联合类型取值的操作

type Union = 1 | 2;
type A = Union[1]; // error

也不能直接将联合类型转为元组

type Union = 1 | 2;
type UnionToTuple<T> = T extends infer F | infer R ? [F, R] : never;
type A = UnionToTuple<Union>; // [1, 1] | [2, 2];

就需要想其他的方法,获取联合类型某个位置的值。

将联合类型转换成函数的交叉类型,通过 infer 推断类型。

在获取函数的返回值上,函数重载和函数交叉类型是一样的。

// 函数重载
type FunctionOverload = {
  (): number;
  (): string;
};
type A = ReturnType<FunctionOverload>; // string

// 函数交叉类型
type Intersection = (() => number) & (() => string);
type B = ReturnType<Intersection>; // string

// 函数重载和函数交叉类型相等
type C = FunctionOverload extends Intersection ? true : false; // true

实现函数交叉类型UnionToIntersectionFn,作用:将联合类型转换成对应的函数交叉类型

你可能会想,这里两个条件判断 extends 肯定都为 true

  • U extends anytrue
  • () extends (k: infer I) => voidtrue

那能不能省略一个或者两个条件判断呢?

不能,这里需要用到泛型的分配率

type Union = 1 | true;
type A = UnionToInfer<Union>;
// 结果=>
type A = (() => 1) & (() => true);

TIP:

type A5 = ((k: 1) => void) | ((k: 2) => void) extends (k: infer I) => void
  ? I
  : never; // never

type A6 = ((k: () => 1) => void) | ((k: () => 2) => void) extends (
  k: infer I
) => void
  ? I
  : never; // (() => 1) & (() => 2)

k 的参数是函数,返回的类型才是交叉类型,其他的都为 never

第二步:实现 GetUnionLast 类型

GetUnionLast 作用是:获取联合类型的最后一个类型

() => infer I 可以用 {(): infer I} 代替

第三步:实现 Prepend 类型

Prepend 作用:把第二步中取到的值放在数组的第一项

接收两个参数 TupleFirst,返回一个数组

  • Tuple 是任意类型的数组
  • First 会任意类型的值,会放入返回数组的第一项

第四步:实现 UnionToTuple 类型

接收三个参数 UnionTLast

  • Union 是任意类型的联合类型
  • T 是任意类型的数组,默认是空数组
  • LastUnion 中最后一位的值,默认 GetUnionLast<Union>

通过判断联合类型中有没有 never 类型,没有 never 递归调用 UnionToTuple

举例: type Union = never | 1 | "s" | true | undefined;

⑴. Union = never | 1 | "s" | true | undefined 代入

  • [Union] extends [never] 代入 Union
    • [never | 1 | "s" | true | undefined] extends [never]false
    • 进入 UnionToTuple<...> 语句
  • UnionToTuple<...>Exclude<...>代入UnionLastPrepend<...> 代入TLast
    • Exclude<never | 1 | "s" | true | undefined, undefined>true | 1 | "s"
    • Prepend<[], undefined>[undefined]
  • 代入结果 UnionToTuple<true | 1 | "s", [undefined]>,进入 ⑵

⑵. Union = true | 1 | "s" 代入

  • [Union] extends [never] 代入 Union
    • [true | 1 | "s"] extends [never]false
    • 进入 UnionToTuple<...> 语句
  • UnionToTuple<...>Exclude<...>代入UnionLastPrepend<...> 代入TLast
    • Exclude<true | 1 | "s", "s">true | 1
    • Prepend<[undefined], "s">["s", undefined]
  • 代入结果 UnionToTuple<true | 1, ["s", undefined]>,进入 ⑶

⑶. Union = true | 1 代入

  • [Union] extends [never] 代入 Union
    • [true | 1] extends [never]false
    • 进入 UnionToTuple<...> 语句
  • UnionToTuple<...>Exclude<...>代入UnionLastPrepend<...> 代入TLast
    • Exclude<true | 1, 1>true
    • Prepend<["s", undefined], 1>[1, "s", undefined]
  • 代入结果 UnionToTuple<true, [1, "s", undefined]>,进入 ⑷

⑷. Union = true 代入

  • [Union] extends [never] 代入 Union
    • [true] extends [never]false
    • 进入 UnionToTuple<...> 语句
  • UnionToTuple<...>Exclude<...>代入UnionLastPrepend<...> 代入TLast
    • Exclude<true, true>never
    • Prepend<[1, "s", undefined], true>[true, 1, "s", undefined]
  • 代入结果 UnionToTuple<never, [true, 1, "s", undefined]>,进入 ⑸

⑸. Union = never 代入

  • [Union] extends [never] 代入 Union
    • [never] extends [never]true
    • 进入 T 语句
  • 输出 T,也就是最终的结果

TIP:在低版本的 ts 中,联合类型无法调用自身,可以将条件类型包装成对象类型。

type UnionToTuple<Union, T extends any[] = [], Last = GetUnionLast<Union>> = {
  0: T;
  1: UnionToTuple<Exclude<Union, Last>, Prepend<T, Last>>; // false分支
}[[Union] extends [never] ? 0 : 1];

方法二

type UnionToIntersection<U> = ?((
  U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never);

type IntersectionToInterFn<I> = UnionToIntersection<
  I extends unknown ? () => I : never
>;

type UnionToTuple<
  Union,
  T extends any[] = []
> = IntersectionToInterFn<Union> extends () => infer R
  ? UnionToTuple<Exclude<Union, R>, [R, ...T]>
  : T;

知识点

思路和方法一一样,将联合类型变为交叉函数类型: UnionToIntersectionIntersectionToInterFn

然后通过条件判断循环调用 UnionToTuple 得到最终的结果。

基础知识

  1. 协变和逆变

9. MyAwaited

题目

题目链接:MyAwaited

实现 MyAwaited ,用来描述 Promise 返回的类型。

import type { Equal, Expect } from "@type-challenges/utils";

type X = Promise<string>;
type Y = Promise<{ field: number }>;
type Z = Promise<Promise<string | number>>;

type cases = [
  Expect<Equal<MyAwaited<X>, string>>,
  Expect<Equal<MyAwaited<Y>, { field: number }>>,
  Expect<Equal<MyAwaited<Z>, string | number>>
];

// @ts-expect-error
type error = MyAwaited<number>;

答案

方法一

type MyAwaited<T extends Promise<any>> = T extends Promise<infer R>
  ? R extends Promise<any>
    ? MyAwaited<R>
    : R
  : never;

知识点

  1. infer 声明 R 来获取 Promise 类型
  2. R extends Promise<any> 判断第二层是不是 Promise 类型
    a. 是 Promise 则递归调用 MyAwaited
    b. 不是 Promise 则返回 Promise 类型 R

基础知识

  1. 通俗易懂的讲解 infer 关键字

TS 内置实用类型

ts 提供一些内置的实用类型,可以在全局使用,官方文档:Utility Types

在做 ts 类型挑战的题目时,发现自己不会这些内置函数,所以在这里记录自己的学习过程。

通俗易懂的讲解 infer 关键字

infer 关键词感觉很玄乎,不太明白怎么用,特别是对类型没有概念的前端来说,类型本身就已经很绕了,再来一个类型推断,就更不能理解了。

infer 简单来讲,可以把它看成变量声明,把它想象成 let 或者 constinfer R 可以看成 let R 或者 const R 声明一个 R 变量,后面会使用到它。

结合示例 ts 内置工具类型 Parameters 来说明

type Parameters<T extends (...args: any) => any> = 
  T extends (...args: infer P) => any ? P : never;

Parameters 作用是接收推断函数参数类型

现在有两个类型 A1A2

  • A1 类型接收的参数是 string 类型,得到的结果就要是 string
  • A2 类型接收的参数是 boolean 类型,得到的结果就要是 boolean
type A1 = Parameters<(str: string) => string>; // => [string]
type A2 = Parameters<(bool: boolean) => string>; // => [boolean]

不用 infer 需要分别实现 Parameters 以满足 stringboolean 不同类型

// 满足 A1 类型
type Parameters<T> = T extends (...arg: string[]) => any ? string[] : never;

// 满足 A2 类型
type Parameters<T> = T extends (...arg: boolean[]) => any ? boolean[] : never;

用这种方式实现就有 2 个问题

  1. 每种类型都要分别实现一个吗?
  2. 类型不确定情况该怎么实现呢?

解决这两个问题就需要用到 infer 关键字

type Parameters<T> = T extends (...arg: infer R) => any ? R : never;

infer 声明一个类型 R,代表函数参数类型,你输入是啥类型就是啥类型,也就是说这里不需要写一个具体的类型了。

所以泛型 T 如果是个函数,它的参数类型就是 R ,返回的就是 R

infer 声明的类型只能在 true 分支中使用。

参考文章

  1. Type inference in conditional types

33. Absolute

题目

题目链接:Absolute答题

实现 Absoulte 类型,接收 stringnumberbigint 类型,返回一个整数字符串

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<Absolute<0>, "0">>,
  Expect<Equal<Absolute<-0>, "0">>,
  Expect<Equal<Absolute<10>, "10">>,
  Expect<Equal<Absolute<-5>, "5">>,
  Expect<Equal<Absolute<"0">, "0">>,
  Expect<Equal<Absolute<"-0">, "0">>,
  Expect<Equal<Absolute<"10">, "10">>,
  Expect<Equal<Absolute<"-5">, "5">>,
  Expect<Equal<Absolute<-1_000_000n>, "1000000">>,
  Expect<Equal<Absolute<9_999n>, "9999">>
];

答案

下面三个方法的思路都是一样的

  • bigintnumber 类型通过 ${T} 可以变成 string 类型
    • 1_000_000n = "1000000"
  • 把负数变成正数
    • -{infer S}S 就是正数

方法一

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

方法二

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

方法三

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

26. Capitalize

题目

题目链接:Capitalize

实现 MyCapitalize,满足下面要求

import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<MyCapitalize<'foobar'>, 'Foobar'>>,
  Expect<Equal<MyCapitalize<'FOOBAR'>, 'FOOBAR'>>,
  Expect<Equal<MyCapitalize<'foo bar'>, 'Foo bar'>>,
  Expect<Equal<MyCapitalize<''>, ''>>,
  Expect<Equal<MyCapitalize<'a'>, 'A'>>,
  Expect<Equal<MyCapitalize<'b'>, 'B'>>,
  Expect<Equal<MyCapitalize<'c'>, 'C'>>,
  Expect<Equal<MyCapitalize<'d'>, 'D'>>,
  Expect<Equal<MyCapitalize<'e'>, 'E'>>,
  Expect<Equal<MyCapitalize<'f'>, 'F'>>,
  Expect<Equal<MyCapitalize<'g'>, 'G'>>,
  Expect<Equal<MyCapitalize<'h'>, 'H'>>,
  Expect<Equal<MyCapitalize<'i'>, 'I'>>,
  Expect<Equal<MyCapitalize<'j'>, 'J'>>,
  Expect<Equal<MyCapitalize<'k'>, 'K'>>,
  Expect<Equal<MyCapitalize<'l'>, 'L'>>,
  Expect<Equal<MyCapitalize<'m'>, 'M'>>,
  Expect<Equal<MyCapitalize<'n'>, 'N'>>,
  Expect<Equal<MyCapitalize<'o'>, 'O'>>,
  Expect<Equal<MyCapitalize<'p'>, 'P'>>,
  Expect<Equal<MyCapitalize<'q'>, 'Q'>>,
  Expect<Equal<MyCapitalize<'r'>, 'R'>>,
  Expect<Equal<MyCapitalize<'s'>, 'S'>>,
  Expect<Equal<MyCapitalize<'t'>, 'T'>>,
  Expect<Equal<MyCapitalize<'u'>, 'U'>>,
  Expect<Equal<MyCapitalize<'v'>, 'V'>>,
  Expect<Equal<MyCapitalize<'w'>, 'W'>>,
  Expect<Equal<MyCapitalize<'x'>, 'X'>>,
  Expect<Equal<MyCapitalize<'y'>, 'Y'>>,
  Expect<Equal<MyCapitalize<'z'>, 'Z'>>,
]

答案

方法一

type MyCapitalize<T extends string> = T extends `${infer F}${infer Rest}`
  ? `${Uppercase<F>}${Rest}`
  : T;

方法二

type AlphaMap = {
  a: "A";
  b: "B";
  c: "C";
  d: "D";
  e: "E";
  f: "F";
  g: "G";
  h: "H";
  i: "I";
  j: "J";
  k: "K";
  l: "L";
  n: "N";
  m: "M";
  o: "O";
  p: "P";
  q: "Q";
  r: "R";
  s: "S";
  t: "T";
  u: "U";
  v: "V";
  w: "W";
  x: "X";
  y: "Y";
  z: "Z";
}

type MyUppercase<T extends string> = T extends `${infer F}${infer Rest}`
  ? F extends keyof AlphaMap
    ? `${AlphaMap[F]}${Rest}`
    : T
  : T;

知识点

自己实现 Uppercase

有一点需要注意:F extends keyof AlphaMap 保证 F 是小写字母

21. Last

题目

题目链接:LastOfArray答题链接

实现 Last,满足下面要求

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<Last<[3, 2, 1]>, 1>>,
  Expect<Equal<Last<[() => 123, { a: string }]>, { a: string }>>,
  Expect<Equal<Last<[]>, undefined>>
];

答案

这题的解题思路有三种

  • 用剩余操作符,直接取最后一项: [...rest, l]
  • 数组长度减一: T["length" - 1]
  • 数组长度加一: T["length" + 1]

方法一

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

知识点

利用剩余操作符,取出最后一项 [...rest, l]

方法二

type Last<T extends unknown[]> = [undefined, ...T][T["length"]];

知识点

利用数组长度加一: T["length" + 1]

方法三

type Last<T extends unknown[]> = T extends [infer F, ...infer R]
  ? T[R["length"]]
  : undefined;

方法四

type LengthOfTuple<T> = T extends { length: infer N } ? N : never;
type DropFirstToTuple<T> = T extends [infer F, ...infer R] ? R : T;
type Last<T extends unknown[]> = T[LengthOfTuple<DropFirstToTuple<T>>];

方法三和方法四思路是一样的,都是利用数组长度减一: T["length" - 1]

方法五

type Last<T extends unknown[]> = T extends [infer F, ...infer R]
  ? R["length"] extends 0
    ? T[0]
    : Last<R>
  : undefined;

利用递归的方式,判断当前数组的长度是不是 0,是 0 的话,说明传入的已经是空数组了,直接取 T[0]

方法六

type HasTail<T> = T extends [] | [unknown] ? false : true;
type Tail<T> = T extends [any, ...infer R] ? R : never;
type Last<T extends unknown[]> = HasTail<T> extends true ? Last<Tail<T>> : T[0];

知识点

HasTail 是利用分配率进行判断的

[] extends [] | [unknown]  // true
[1] extends [] | [unknown]  // true
[1, 2] extends [] | [unknown]  // false

利用递归的方式,直到元组只剩最后一项,用 T[0] 的方式取出来

31. Flatten

题目

题目链接:Flatten解答

实现 Flatten,将多维元组打平

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<Flatten<[]>, []>>,
  Expect<Equal<Flatten<[1, 2, 3, 4]>, [1, 2, 3, 4]>>,
  Expect<Equal<Flatten<[1, [2]]>, [1, 2]>>,
  Expect<Equal<Flatten<[1, 2, [3, 4], [[[5]]]]>, [1, 2, 3, 4, 5]>>,
  Expect<
    Equal<
      Flatten<[{ foo: "bar"; 2: 10 }, "foobar"]>,
      [{ foo: "bar"; 2: 10 }, "foobar"]
    >
  >
];

答案

方法一

type Flatten<T extends unknown[]> = T extends [infer F, ...infer Rest]
  ? F extends unknown[]
    ? [...Flatten<F>, ...Flatten<Rest>]
    : [F, ...Flatten<Rest>]
  : [];

// 另一种写法
type Flatten<T extends unknown[]> = T extends [infer F, ...infer Rest]
  ? F extends unknown[]
    ? Flatten<[...F, ...Rest]>
    : [F, ...Flatten<Rest>]
  : [];

// 另一种写法
type Flatten<T extends unknown[]> = T extends [infer F, ...infer R]
  ? [...(F extends unknown[] ? Flatten<F> : [F]), ...Flatten<R>]
  : [];

知识点

要将多维元组变成一维元组,需要判断元组的每一项是不是元组

  1. 在元组中,通过 infer F 取出第一项,剩余的放在 ...infer Rest
  2. 判断 F 是不是元组,如果是元组,通过递归调用 Flatten,将第一项打平,同时将 Rest 也打平
  • 因为 ...infer Rest 将元组剩余部分升维了
  1. 如果只是因为降维,那使用 ...Rest 就行的,但这里为什么要使用 ...Flatten<Rest>
  • 因为 Rest 是元组,元组就要同步进行打平
  1. 回到第二步,如果 F 不是元组,就将 F 作为元组的第一项,剩余的部分接着打平 Fletten<Rest>

ps:true 分支处理 [[1], [2, 3, [4], 5] 情况,false 分支处理 [1, [2, 3, [4], 5]]

方案二

type Flatten<T> = T extends []
  ? []
  : T extends [infer F, ...infer Rest]
  ? [...Flatten<F>, ...Flatten<Rest>]
  : [T];

知识点

思路:如果不是元组,则将这个类型变成元组

  1. 先判断的 T 是不是空元组,如果是[],直接返回 []
  2. 如果 T 不是空元组,取出第一项 F 和剩余项 Rest,分别打平
  3. 递归中的 false 分支最终会反应到 true 分支中

ps:此方案有两个问题:

  • 不能限制泛型 T 的类型
  • 传入的不是元组,将会输出会将这个类型变成元组,而不是报错,例如:Flatten<"tuple"> => ["tuple"]

方法三

type Flatten<T> = T extends unknown[]
  ? T extends [infer A, ...infer R]
    ? [...Flatten<A>, ...Flatten<R>]
    : []
  : [T];

知识点

和方法二的判断逻辑刚好相反,也存在同样的问题

  1. 先判断 T 是不是任意类型的元组
  • 如果是元组,则将它打平
  • 如果不是元组,则将这个类型变成元组

30. LengthOfString

题目

题目链接:LengthOfString解答

实现 LengthOfString,获取字符串的长度:

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<LengthOfString<"">, 0>>,
  Expect<Equal<LengthOfString<"kumiko">, 6>>,
  Expect<Equal<LengthOfString<"reina">, 5>>,
  Expect<Equal<LengthOfString<"Sound! Euphonium">, 16>>
];

答案

方法一

type StringToArray<T extends string> = T extends `${infer F}${infer Rest}`
  ? [F, ...StringToArray<Rest>]
  : [];
type LengthOfString<T extends string> = StringToArray<T>["length"];

知识点

  1. js 中计算字符串的长度,直接用 length 就可以获取字符串的长度
  2. 所以在 ts 类型中,"aa"["length"] 得到的结果是 number
  3. length 属性的还有元组类型
  4. 通过将字符串转换成元组,在取它的 length 属性,不就拿到元组的长度了么
  5. 元组的长度也就是字符串的长度

方法二

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

方法三

type LengthOfString<T extends string, R extends string[] = []> = T extends ""
  ? R["length"]
  : T extends `${infer F}${infer Rest}`
  ? LengthOfString<Rest, [F, ...R]>
  : never;

知识点

方法二、方法三的思路和方法一是一样的,区别在于:

  • 方法二、方法三将字符串转换成的元组作为自己的第二个参数
  • 方法一是生成一个独立的元组

联合类型和交叉类型

基本类型中和元组中,联合类型取的是并集,交叉类型取的是交集

type A = 1 | 2 | 3;
type B = 1 | 2 | 4;

type C = A & B; // 1 | 2
type D = A | B; // 1 | 2 | 3 | 4

在对象中,联合类型取的类型是属性交集,交叉类型取的类型是属性并集

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

type B = {
  name: string;
  gender: string;
}

type C = A | B; // => { name: string; }
type D = A & B; // => { name: string; age: number; gender: string; }

如果换个角度,对象的联合类型是对象的并集

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

type B = {
  name: string;
  gender: string;
}

type C = A | B; // A 或者 B

const c: C = { name: "1", age: 1, gender: "1" };
c.age = 2; // error

const c1: C = { name: "1", age: 1 };
c1.age = 2;

const c2: C = { name: "1", gender: "1" };
c2.gender = "2";

23. PromiseAll

题目

题目链接:PromiseAll

实现 PromiseAll,满足下面功能

import type { Equal, Expect } from '@type-challenges/utils'

const promiseAllTest1 = PromiseAll([1, 2, 3] as const)
const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])

type cases = [
  Expect<Equal<typeof promiseAllTest1, Promise<[1, 2, 3]>>>,
  Expect<Equal<typeof promiseAllTest2, Promise<[1, 2, number]>>>,
  Expect<Equal<typeof promiseAllTest3, Promise<[number, number, number]>>>,
]

答案

方法一

type TUtil1<T> = T extends Promise<infer R> ? R : T;
type TUtil2<T extends unknown[]> = T extends [infer F, ...infer R]
  ? [TUtil1<F>, ...TUtil2<R>]
  : [];
declare function PromiseAll<T extends unknown[]>(
  values: readonly [...T]
): Promise<TUtil2<T>>;

方法二

declare function PromiseAll<T extends unknown[]>(
  values: readonly [...T]
): Promise<{ [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K] }>;

解析

遍历的方法有两种:

  • 递归(方法一)
  • 使用 in 变量(方法二)

方法一使用了可变元组类型:(Variadic tuple types)[https://github.com/microsoft/TypeScript/pull/39094]

`4` 种类型约束

ts 中,经常会用到类型约束,用来确保使用的类型是正确的,介绍 4 种类型约束

  1. 传入只能是数组,在传入的时候做约束,这种约束见的比较多

    type A<T extends unknown[]>
  2. 利用 extends 条件类型进行类型约束

    // 如果 T 是 string,则输出 T
    // 如果 T 不是 string,则输出 never
    type B<T> = T extends string ? T : never;
  3. 利用联合类型进行条件约束

    // 如果 T 是 string,则输出 T
    // 如果 T 不是 string,则输出 never
    type C<T> = T & string;
  4. typescript 4.7 推出了一个新的条件约束,利用 extends 语法

    // F 进行了类型约束,是 string 类型
    type D<T extends unknown> = T extends [
      infer F extends string,
      ...infer Rest
    ] ? F : never;
    
    // 在 4.7 之前,如果要保证 F 是 string 类型要用联合类型的形式
    type D<T extends unknown> = T extends [
      infer F,
      ...infer Rest
    ] ? F & string: never;

15. TupleToObject

题目

题目链接:TupleToObject

实现 TupleToObject ,将数组变为对象,对象的每项 key = value,为数组的每项

import type { Equal, Expect } from "@type-challenges/utils";

const tuple = ["tesla", "model 3", "model X", "model Y"] as const;

type cases = [
  Expect<
    Equal<
      TupleToObject<typeof tuple>,
      {
        tesla: "tesla";
        "model 3": "model 3";
        "model X": "model X";
        "model Y": "model Y";
      }
    >
  >
];

// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>;

答案

方法一

type TupleToObject<T extends readonly (keyof any)[]> = {
  [key in T[number]]: key;
};

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [key in T[number]]: key;
};

type TupleToObject<T extends readonly PropertyKey[]> = {
  [key in T[number]]: key;
};

type TupleToObject<T extends ReadonlyArray<PropertyKey>> = {
  [key in T[number]]: key;
};

知识点

keyof anystring | number | symbol

keyof 和类型访问

keyof

可以把 keyof 简单的理解为相遍历,把遍历的结果作为联合类型返回。

用官网的例子讲解,首先定义一个 Person 接口

interface Person {
  name: string;
  age: number;
  location: string;
}
  1. 遍历 Person 接口的属性,将得到的值是 nameagelocation 的联合类型

    type K1 = keyof Person; // "name" | "age" | "location"
  2. Person[] 是个数组,遍历的是数组的属性,得到的就是数组属性的联合类型

    type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
  3. 就是一个对象,对象的 keystring 类型,所以得到 string ,但为什么是 stringnumber 的联合类型,还不太清楚。

    如果把 string 换成了 number ,那么得到的结果是 nubmer

    type K3 = keyof { [x: string]: Person }; // string | number
    type K4 = keyof { [x: number]: Person }; // number

类型访问

  1. 类型也可以通过访问的形式得到, Person["name"] 因为 namestring 所以得到 string

    type P1 = Person["name"]; // string
  2. namestringagenumber ,得到 stringnumber 的联合类型

    type P2 = Person["name" | "age"]; // string | number
  3. stringcharAt 是函数,得到 charAt 函数类型

    type P3 = string["charAt"]; // (pos: number) => string
  4. stringpush 是函数,得到 push 函数类型

    type P4 = string[]["push"]; // (...items: string[]) => number
  5. 访问数组的第一项,类型是 string

    type P5 = string[][0]; // string

17. UnionToIntersection

题目

题目链接:UnionToIntersection

实现 UnionToIntersection ,将联合类型变为交叉类型

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<UnionToIntersection<"foo" | 42 | true>, "foo" & 42 & true>>,
  Expect<
    Equal<
      UnionToIntersection<(() => "foo") | ((i: 42) => true)>,
      (() => "foo") & ((i: 42) => true)
    >
  >
];

答案

利用逆变的原理:联合类型 → 交叉类型函数 → 交叉类型

方法一

type UnionToIntersection<U> = (
  U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

知识点

  1. 将联合类型 U 变为 (k: U) => voidU extends unknown ? (k: U) => void : never

    "foo" | 42 | true -> ((k: "foo") => void) | ((k: 42) => void) | ((k:  true) => void)
  2. 根据函数的逆变可以得到

    (k: "foo") => void  <=  (k: "foo" & 42 & true) => void
    (k: 42) => void     <=  (k: "foo" & 42 & true) => void
    (k: true) => void   <=  (k: "foo" & 42 & true) => void
  3. 最后类型推导的结果是 "foo" & 42 & true

方法二

type UnionToIntersection<U> = {
  [key in U as symbol]: (k: U) => void;
} extends {
  [k: symbol]: (k: infer I) => void;
}
  ? I
  : never;

知识点

和方法一是一样的,它是用对象的形式去表达交叉类型函数

这个方法在文档里有介绍:Type inference in conditional types

基础知识

  1. 协变和逆变

ts 中的一些区别

typeinterface 区别

  1. utility type 要用 type 定义
  2. 需要继承的接口用 interface 定义

6. MyReturnType

题目

题目链接:MyReturnType

实现 ReturnType 返回函数的返回类型

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<string, MyReturnType<() => string>>>,
  Expect<Equal<123, MyReturnType<() => 123>>>,
  Expect<Equal<ComplexObject, MyReturnType<() => ComplexObject>>>,
  Expect<Equal<Promise<boolean>, MyReturnType<() => Promise<boolean>>>>,
  Expect<Equal<() => "foo", MyReturnType<() => () => "foo">>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn>>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn1>>>
];

type ComplexObject = {
  a: [12, "foo"];
  bar: "hello";
  prev(): number;
};

const fn = (v: boolean) => (v ? 1 : 2);
const fn1 = (v: boolean, w: any) => (v ? 1 : 2);

答案

方法一

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

方法二

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

解析

方法一和方法二是一样的, MyReturnType 泛型 T 越不约束都行,因为后面都要进行约束,也就是说方法一更严谨一点。

基础知识

  1. ReturnType

协变和逆变

协变和逆变的概念网上一搜一大把,就不说了,说概念一定会把人绕晕,举个例子来说就容易理解了。

假设现在有个父子关系: ParentSon

interface Parent {
  parentProp: string;
}
interface Son extends Parent {
  sonProp: string;
}

type IsCo = Son extends Parent ? true : false; // true

变化规则为:f ⇒ 把 ParentSon 变为数组:

type ParentArray = Parent[];
type SonArray = Son[];

type IsCo = SonArray extends ParentArray ? true : false; // true

因为 SonParent 的子类型,他们都变为数组后,还是满足 SonParent 的子类型,所以这个叫做协变。

此时把变化规则 f 改为函数 ⇒ 两个函数分别接收 ParentSon

type ParentFn = (param: Parent) => void;
type SonFn = (param: Son) => void;

type IsContra = ParentFn extends SonFn ? true : false; // true

此时他们的继承关系反过来了,因为 ParentSon 的子集,或者叫安全类型,所以这叫做逆变

就有下面的公式

  • f(Son) ≤ f(Parent) 变化规则 f 被称为:协变
  • f(Parent) ≤ f(Son) 变化规则 f 被称为:逆变

逆变的应用

逆变最常见的应用是函数重载

function fn(x: never): void;
function fn(x: string): void {}

// 等价于

function fn(x: number & string): void;
function fn(x: string): void {}

neverstring 的子类型。

never 也可以是交叉类型的结果,如 number & stringnever

题目

题目链接:UnionToIntersection

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<UnionToIntersection<"foo" | 42 | true>, "foo" & 42 & true>>,
  Expect<
    Equal<
      UnionToIntersection<(() => "foo") | ((i: 42) => true)>,
      (() => "foo") & ((i: 42) => true)
    >
  >
];

答案为

type UnionToIntersection<U> = (
  U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;
  1. 首先将联合类型变为 (k: Union) => void 的形式:

    "foo" | 42 | true -> ((k: "foo") => void) | ((k: 42) => void) | ((k:  true) => void)
  2. 根据函数的逆变,可以得到

    (k: "foo") => void  <=  (k: "foo" & 42 & true) => void
    (k: 42) => void     <=  (k: "foo" & 42 & true) => void
    (k: true) => void   <=  (k: "foo" & 42 & true) => void
  3. 最后类型推导的结果是 "foo" & 42 & true

其实看到最后的结果是有点懵的,为什么就是交叉类型呢?

把它想象成函数的重载,就好理解了

function fn(k: "foo" & 42 & true): void;
function fn(k: "foo"): void {}
// 42 和 true 同样的道理

传入什么类型可以保证 "foo" 类型安全呢?

一定是 "foo" 的子类型

"foo" 的子类型是什么呢?

never

如何得到 never 呢?

"foo" & 42 & true

TIP

要明确一个概念交叉类型 IntersectionType 是交集,联合类型 UnionType 是并集

所以 string & number & boolean < string < string | number | boolean

参考文章

  1. 大白话聊 TypeScript 中的变型
  2. 聊聊TypeScript类型兼容,协变、逆变、双向协变以及不变性,评论区这段话很受启发:

    狗是动物,所以一群狗是一群动物,这是协变。我需要一张能刷人民币的信用卡,你给了我一张任意币种信用卡,这是符合要求的,即,人民币是任意货币的一种,任意币种信用卡是能刷人民币的信用卡,这个推论反过来了,叫逆变。

条件类型:extends 用法

普通条件类型

LeftType extends RightType ? TrueType : FalseType;

判断 LeftType 能否分配给 RightTypeRightType 一切约束条件,LeftType 都具备,也就是说 RightType 有的值 LeftType 必须要有。

type A = {
  name: string;
};

type B = {
  name: string;
};

type Bool = A extends B ? true : false; // Bool -> true

这里面 A 类型和 B 类型完全相同,两个类型都有 name 属性,所以输出为 true

type A = {
  name: string;
};

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

type Bool = A extends B ? true : false; // Bool -> false

B 类型比 A 类型多了一个 age 属性,也就是说 A 类型不满足 B 类型的所有条件,所以输出为 false

高阶条件类型

分配率

type A1 = "x" extends "x" ? true : false; // A1 -> true
type A2 = "x" | "y" extends "x" ? true : false; // A2 -> false

这个很好理解,和上面普通条件类型是一样的。

"x" | "y" extends "x" 竖线是或的意思,这里 "y" extends "x"false,所以结果是 false

type P<T> = T extends "x" ? true : false;
type A3 = P<"x">; // A3 -> true
type A4 = P<"x" | "y">; // A4 -> boolen

P 是带参数 T 的泛型类型,其表达式和 A1A2 形式完全一样。 A3A4 是泛型类型 P 传入参数得到的类型, 代入参数后的形式和 A1A2 形式完全一样,为什么 A4 的结果是 boolean 不是 false 呢?

type P<T> = T extends "x" ? true : false;
type A4 = P<"x" | "y">;

/** 代入过程 **/

type A4 = P<"x"> | P<"y">;
// =>
type A4 = ("x" extends "x" ? true : false) | ("y" extends "x" ? true : false);

因为 T extends "x" 会应用分配率,将 "x""y" 拆开,分别代入 P<T> ,最后将结果联合起来。

特殊的 never 满足分配率

// never 是所有类型的子类型
type A5 = never extends "x" ? true : false; // A5 -> true

type P<T> = T extends "x" ? true : false;
type A6 = P<never>; // A6 -> never

A6 的结果和 A5 居然不一样,因为 never 是所有类型的子类型(包括联合类型),所以它还是满足分配率的,只是 never 没有类型可以分配,也就没有执行后面的表达式,结果当然就是 never 了。

防止泛型使用分配率

type P<T> = [T] extends ["x"] ? true : false;

type A4 = P<"x" | "y">; // A4 -> false;
type A6 = P<never>; // A6 -> true;

[] 括起来,此时传入的泛型参数将被当做一个整体,不在分配。

要注意的是 extends 左右两个类型都要加上 [] ,否则没有用。

总结

要满足分配率有两个条件:

  1. 参数是泛型
  2. 代入的参数是联合类型

参考

Distributive Conditional Types

28. AppendArgument

题目

题目链接:AppendArgument

实现 AppendArgument,满足下面需求,将第二个参数放到函数参数的最后一位

import type { Equal, Expect } from "@type-challenges/utils";

type Case1 = AppendArgument<(a: number, b: string) => number, boolean>;
type Result1 = (a: number, b: string, x: boolean) => number;

type Case2 = AppendArgument<() => void, undefined>;
type Result2 = (x: undefined) => void;

type cases = [Expect<Equal<Case1, Result1>>, Expect<Equal<Case2, Result2>>];

答案

方法一

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

知识点

  1. 通过 infer Args 获取到参数类型
  2. 把函数的参数和传入的第二个参数使用元组,组合在一起
  3. 这种方法有个问题:函数的形参不在是传入的形参了,而是变成 args_0args_1 ...

方法二

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

知识点

解决方法二中,形参不是传入的形参问题。

方法三

type AppendArgument<Fn extends (...args: any) => any, A> = (
  ...args: [...Parameters<Fn>, A]
) => ReturnType<Fn>;

知识点

  1. Parameters 可以获取到函数的参数
  2. ReturnType 可以获取到函数的返回值

29. MyParameters

题目

题目链接:MyParameters

实现 MyParameters,获取函数的参数类型

import type { Equal, Expect } from "@type-challenges/utils";

const foo = (arg1: string, arg2: number): void => {};
const bar = (arg1: boolean, arg2: { a: "A" }): void => {};
const baz = (): void => {};

type cases = [
  Expect<Equal<MyParameters<typeof foo>, [string, number]>>,
  Expect<Equal<MyParameters<typeof bar>, [boolean, { a: "A" }]>>,
  Expect<Equal<MyParameters<typeof baz>, []>>
];

答案

方法一

type MyParameters<Fn> = Fn extends (...args: infer Args) => void ? Args : never;

7. GetRequired

题目

题目链接:GetRequired

实现 GetRequired ,返回所有的必需字段。

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<GetRequired<{ foo: number; bar?: string }>, { foo: number }>>,
  Expect<
    Equal<GetRequired<{ foo: undefined; bar?: undefined }>, { foo: undefined }>
  >
];

答案

Person 类型举例

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

方法一

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

type FilterKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];

type GetRequired<T> = {
  [K in FilterKeys<MapToKeys<T>>]: T[K];
};
type MapToNever<T> = {
  [K in keyof T]: never;
};

type PickNever<T> = {
  [K in keyof T]-?: T[K] extends never ? K : never;
}[keyof T];

type GetRequired<T> = {
  [K in PickNever<MapToNever<T>>]: T[K];
};

知识点

上下两种方法是一样的,这里讲左边的方法。

  1. MapToKeys 作用把 key = value

    type T1 = MapToKeys<Person>
    
    // 结果 =>
    
    T1 = {
      name: "name";
      age?: "age" | undefined;
    }
  2. FilterKeys 将可选的 key 过滤掉

    type T2 = FilterKeys<Person>;
    
    // 结果 =>
    
    T2 = string;

    解析

    1. {[K in keyof T]-?: undefined extends T[K] ? never : K}
      1. keyof T 的结果是联合类型,这里是 "name" | "age"
      2. K in keyof TK 结果是 name 或者 age
      3. -? 是去除可选项
      4. 结果: { name: string; age: never; }
        • undefined extends number | undefinedtrue
        • undefined extends stringfalse
    2. a 的结果上加 [keyof T] 结果是 string | neverstring
  3. 使用 MapToKeysPersonkey = value,这样 b 的结果就是 name

  4. {[K in 第二步的结果]: T[K]} 遍历取值,就得到最终的结果

tips

  1. -?将可选项去掉,让这个类型变成必选项,+? 增加可选项,让这个类型变成可选项
  2. 为什么上面使用 undefined extends T[K] ,下面使用 T[K] extends never
    1. [K in keyof T]: string
      1. 可选项的值 K = string | undefined
      2. 必选项的值 K = string
    2. [K in keyof T]: never
      1. 可选项的值 K = undefined
      2. 必选项的值 K = never
    3. 上面的 T[K] 要放到 extends 右边,下面的 T[K] 要放到 extends 的左边

方法二

type GetRequired<T> = Pick<
  T,
  { [key in keyof T]-?: {} extends Pick<T, key> ? never : key }[keyof T]
>;

知识点

思路和方法一是一样:

  1. 这里使用 Pick<T, key>T 中挑选出 key

    解析: [key in keyof T]-?: {} extends Pick<T, key> ? never : key

    1. {} extends {age?: number}true
    2. {} extends {name: string}false
    3. 这里为什么不用 Pick<T, keyof T> 是因为 keyof T 的结果是个联合类型,也就是说 Pick<T, keyof T> = T
  2. {[key in keyof T]-?: {} extends Pick<T, key> ? never : key}[keyof T] 结果是

    1. {name: string; age: never}["name" | "age"]string | neverstring
  3. 使用 Pick<T, 第二步的结果> 得到最终的结果

方法三

type GetRequired<T> = {
  [key in keyof T as {} extends Pick<T, key> ? never : key]: T[key];
};

知识点

  1. 思路和方法二是一样的,使用 Pick<T, key>T 中挑选出 key
  2. {} extends Pick<T, key>true 说明空对象和纯可选是一致的
  3. 通过 as 可以拆分成两部分,as 左边是结果,右边是约束条件
    1. key = age 时,{} extends Pick<Person, "age">true{[never]: Person["age"]} ⇒ 空
    2. key = name 时,{} extends Pick<Person, "name">false{["name"]: Person["name"]}{name: string}

方法四

type GetRequired<T> = {
  [key in keyof T as T[key] extends Required<T>[key] ? key : never]: T[key];
};

// 等价于
type GetRequired<T, P extends Required<T> = Required<T>> = {
  [key in keyof T as T[key] extends P[key] ? key : never]: T[key];
};

// 等价于
type GetRequired<T> = {
  [key in keyof T as T[key] extends { [K in keyof T]-?: T[K] }[key]
    ? key
    : never]: T[key];
};

// 等价于
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type GetRequired<T, P extends MyRequired<T> = MyRequired<T>> = {
  [key in keyof T as T[key] extends P[key] ? key : never]: T[key];
};

方法五

type PossiblyRequiredKeys<T> = {
  [K in keyof T]: undefined extends T[K] ? never : K;
}[keyof T];

type DefinitelyRequiredKeys<T, R extends T = Required<T>> = {
  [K in keyof R]: undefined extends R[K] ? K : never;
}[keyof R];

type GetRequired<T> = Pick<
  T,
  PossiblyRequiredKeys<T> | DefinitelyRequiredKeys<T>
>;

方法六

type GetRequired<T> = {
  [key in keyof T extends infer K
    ? K extends keyof T
      ? {} extends Pick<T, K>
        ? never
        : K
      : never
    : never]: T[key];
};

不理解到理解系列

as 的用法

在映射时经常会用到 as 关键词,像下面代码一样

type MappedKey<T> = {
  [K in keyof T as K extends xxx ? never : K]: T[K];
};

一开始理解的是 as 左边要满足右边的条件,

其实不是的,这里 as 后面的语句使用来做筛选的

遍历时得到的 K 需要做一些特殊的处理时,比如给 K 加上前缀或后缀,筛选符合某种条件的 K,用 as 连接条件语句

as 是 typescript 4.1 功能,文档链接: Key Remapping in Mapped Type

5. MyReadonly

题目

题目链接:MyReadonly

实现 Readonly 使得属性变为可读。

当尝试修改 titledescription 会报错

interface Todo {
  title: string;
  description: string;
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar",
};

todo.title = "Hello"; // Error: cannot reassign a readonly property
todo.description = "barFoo"; // Error: cannot reassign a readonly property

答案

方法一

type MyReadOnly<T extends {}> = { readonly [key in keyof T]: T[key] };

知识点

  1. 使用 keyofT 中的属性变成联合类型
  2. 使用 in 遍历联合类型
  3. 最后用 readonly 去修饰每一个 key

方法二

type MyReadonly<T, K extends keyof T = keyof T> = {
  readonly [key in K]: T[key];
};

知识点

这里做了两次约束

  1. 第一个约束是 K extends keyof TKT 属性的联合类型
  2. 第二个约束是 = keyof T ,如果没有为该类型指定类型参数,默认使用 keyof T

基础知识

  1. 默认约束 = keyof T

4. FirstOfArray

题目

题目链接:FirstOfArray

实现一个 FirstOfArray 满足测试用例 cases,作用是返回数组中的第一项

  • 测试用例1: [3, 2, 1] 结果为 3
  • 测试用例2: [() => 123, { a: string }] 结果为 () => 123
  • 测试用例3: [] 结果为 never
  • 测试用例4: [undefined] 结果为 undefined
import { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<FirstOfArray<[3, 2, 1]>, 3>>,
  Expect<Equal<FirstOfArray<[() => 123, { a: string }]>, () => 123>>,
  Expect<Equal<FirstOfArray<[]>, never>>,
  Expect<Equal<FirstOfArray<[undefined]>, undefined>>
];

type errors = [
  // @ts-expect-error
  FirstOfArray<"notArray">,
  // @ts-expect-error
  FirstOfArray<{ 0: "arrayLike" }>
];

答案

方法一

type FirstOfArray<Arr extends any[]> = 
  Arr extends [infer Item, ...any[]] ? Item : never;

知识点

  1. 泛型 Arr 继承自任意类型的数组
  2. infer Item 是声明一个 Item,类似于变量声明,在后面都可以使用
  3. [infer Item, ...any[]] 取出数组的第一项,放在变量 Item
    1. 通过 extends 判断泛型 Arr 是否继承自 [infer Item, ...any[]]
    2. 空数组 [] extends [infer Item, ...any[]] 结果为 false,所以输出为 never
    3. 其余情况为 true,输出数组第一项

方法二

type FirstOfArray<Arr extends any[]> = Arr extends [] ? never : Arr[0];

知识点

  1. [] extens []true 输出为 never
  2. 其余情况为 false 输出为 Arr[0]

方法三

type FirstOfArray<Arr extends any[]> = "0" extends keyof Arr ? Arr[0] : never;

知识点

  1. keyof Arr 取出数组中的每一个 key
  2. "0" extends keyof Arr 判断数组中有没第一项,因为空数组是没有第一项的。
  3. 如果有第一项,输出 Arr[0] 否则为 never

方法四

type FirstOfArray<Arr extends any[]> = Arr["length"] extends 0 ? never : Arr[0];

知识点

  1. Arr["length"] 可以获取泛型 Arr 的长度
  2. Arr["length"] extends 0 判断数组长度是否为 0
  3. 如果为 0 输出 never,否者输出 Arr[0]

方法五

type FirstOrArray<Arr extends any[]> = 
  Arr[number] extends never ? never : Arr[0];

知识点

  1. Arr[number] 可以得到数组的每一项值的联合类型

    type A1<Arr> = Arr[number]
    type A2: A1<[1, 2, 3]> // => 1 | 2 | 3
    
    type A3: A1<[]> // => never
  2. 如果是个空数组 Arr<[]> 会返回 never,通过判断 Arr[number] extends never 是否为 never

  3. 如果为 never 输出 never 否则输出 Arr[0]

方法六

type FirstOfArray<Arr extends any[]> = Arr[Extract<keyof Arr, "0">];

总结

空数组的几种判断方法

  • T extends []
  • T extends never[]
  • "0" extends keyof T
  • T["length"] extends 0
  • T[number] extends never

基础知识

  1. extends 用法
  2. 通俗易懂的讲解 infer 关键字

19. TupleToNestedObject

题目

题目链接:TupleToNestedObject回答链接

实现 TupleToNestedObject,符合下面要求

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<TupleToNestedObject<["a"], string>, { a: string }>>,
  Expect<Equal<TupleToNestedObject<["a", "b"], number>, { a: { b: number } }>>,
  Expect<
    Equal<
      TupleToNestedObject<["a", "b", "c"], boolean>,
      { a: { b: { c: boolean } } }
    >
  >,
  Expect<Equal<TupleToNestedObject<[], boolean>, boolean>>
];

答案

方法一

type TupleToNestedObject<T extends unknown[], R> = T extends [
  infer F extends string,
  ...infer Rest
]
  ? { [K in F] : TupleToNestedObject<Rest, R> }
  : R;

方法二

type TupleToNestedObject<T extends unknown[], R> = T extends [
  infer F,
  ...infer Rest
]
  ? TupleToNestedObject<Rest, { [K in F & string]: R }>
  : R;

方法三

type TupleToNestedObject<T extends unknown[], R> = T extends [
  infer F,
  ...infer Rest
]
  ? Record<F & string, TupleToNestedObject<Rest, R>>
  : R;

知识点

方法一、方法二、方法三的思路都是一样的,取出数组的第一项作为 key,后面作为新的参数递归调用 TupleToNestedObject,得到最终的 value

38. PickByType

题目

题目链接:PickByType答题

实现 PickByType,满足下面需求

import type { Equal, Expect } from "@type-challenges/utils";

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

type cases = [
  Expect<
    Equal<
      PickByType<Model, boolean>,
      { isReadonly: boolean; isEnable: boolean }
    >
  >,
  Expect<Equal<PickByType<Model, string>, { name: string }>>,
  Expect<Equal<PickByType<Model, number>, { count: number }>>
];

答案

方法一

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

知识点

使用 as 可以对 K 进行过滤

方法二

type FilterKey<T, U, K extends keyof T = keyof T> = K extends K
  ? T[K] extends U
    ? K
    : never
  : never;
type PickByType<T, U> = {
  [K in FilterKey<T, U>]: U;
};

知识点

ts 4.1 之前,没有 as 的情况下如果对 K 进行过滤?

  • 自定义 FilterKey 类型,它的核心是利用泛型的分配:K extends K,前面的 K 会发生分配

方法三

type PickByType<T, U> = Pick<
  T,
  { [K in keyof T]: T[K] extends U ? K : never }[keyof T]
>;

1. MyPick

题目

题目链接:MyPick

实现一个 MyPick 满足测试用例 cases

  • Expected1 满足一个属性:title
  • Expected2 满足两个属性:titlecompleted
import { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<Expected1, MyPick<Todo, "title">>>,
  Expect<Equal<Expected2, MyPick<Todo, "title" | "completed">>>,
  // @ts-expect-error
  MyPick<Todo, "title" | "completed" | "invalid">
];

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

interface Expected1 {
  title: string;
}

interface Expected2 {
  title: string;
  completed: boolean;
}

答案:

方法一

type MyPick<Type, Keys extends keyof Type> = {
  [key in Keys]: Type[key];
};

解析

使用 in 遍历 Keys ,和对象一样的取值方法 Type[key]

方法二

type MyPick<Type, Keys extends keyof Type> = Omit<Type, keyof Omit<Type, Keys>>;

解析

使用 Omit 做两次剔除,实现 Pick 的效果了

  1. 第一次 Omit<Type, Keys> 剔除掉 TypeKeys 属性
  2. 第二次 Omit<Type, keyof Omit<Type, Keys>> 剔除掉 1 中的结果

方法三

type MyPick<Type, Keys extends keyof Type> = Omit<
  Type,
  Exclude<keyof Type, Keys>
>;

解析

使用 ExcludeOmit 做两次剔除,实现 Pick 的效果

  1. 第一次 Exclude<keyof Type, Keys> 剔除掉 Type 中的 Keys
  2. 第二次 Omit<Type, Exclude<keyof Type, Keys>> 剔除掉 1 中的结果

方法四

type MyPick<Type, Keys> = {
  [key in keyof Type & Keys]: Type[key];
};

解析

这个方法有个问题,如果 Keys 不是 Type 的键时,它不会报错。

type invalid = MyPick<Todo, "title" | "something">;

方法五

type MyPick<Type, Keys> = {
  [key in keyof Type as key extends Keys ? key : never]: Type[key];
};

解析

问题同上面一样

key extends Keys ? key : never 这是一个整体

知识点:

  1. 如何获取到所有的 key

    通过 keyof 可以获取到所有的 key

  2. 如何遍历所有的 key

    使用 in 操作符,可以遍历所有的 key

基础知识

  1. extends 用法
  2. Pick
  3. Exclude
  4. Omit
  5. Omit、Pick、Exclude 的区别

默认约束 = keyof T

type MyReadonly<T, K extends keyof T = keyof T> = {
  readonly [key in K]: T[key];
};

上面的例子第二个参数 = keyof T ,它的作用是为 T 指定类型参数,默认值为 keyof T

keyof T 好理解,表示 T 中所有属性的联合类型。

interface Todo {
  title: "string";
  description: "string";
}

const todo: MyReadonly<Todo> = {
  title: "hey",
  description: "foobar",
};
todo.title = "hello"; // => error
todo.description = "barfoo"; // => error

const todo2: MyReadonly<Todo, "title"> = {
  title: "hey",
  description: "foobar", // => error
};
todo2.title = "hello"; // => error

todo 没有传入第二个参数,默认的约束是 titledescription,必须要传入 titledescription

todo2 传入了第二个参数 title ,此时的约束变成了 title,如果在 todo2 再写一个 description 将报错。

2. MyOmit

题目

题目链接:MyOmit

实现一个 MyOmit 满足测试用例 cases

  • Expected1 满足两个属性:titlecompleted
  • Expected2 满足一个属性:title
type cases = [
  Expect<Equal<Expected1, MyOmit<Todo, "description">>>,
  Expect<Equal<Expected2, MyOmit<Todo, "description" | "completed">>>
];

// @ts-expect-error
type error = MyOmit<Todo, "description" | "invalid">;

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

interface Expected1 {
  title: string;
  completed: boolean;
}

interface Expected2 {
  title: string;
}

答案

方法一

type MyOmit<Type, Keys extends keyof Type> = Pick<
  Type,
  Exclude<keyof Type, Keys>
>;

解析

使用 PickExclude 可以实现 Omit

  1. Exclude<keyof Type, Keys> 使用 Exclude 剔除掉 Type 中的 key
  2. Pick<Type, Exclude<keyof Type, Keys>> 使用 Pick 挑选出 1 中的结果

方法二

type MyOmit<Type, Keys extends keyof Type> = {
  [key in Exclude<keyof Type, Keys>]: Type[key];
};

解析

使用 Exclude 可以实现 Omit

  1. Exclude<keyof Type, Keys> 使用 Exclude 剔除掉 Type 中的 Keys
  2. 使用 in 遍历 1 中的结果
  3. 使用和对象一样的取值方法 Type[key]

基础知识

  1. extends 用法
  2. Pick
  3. Exclude
  4. Omit
  5. Omit、Pick、Exclude 的区别

keyof 取值

使用 keyofanynever 取值

keyof any; // string | number | symbol
keyof never; // string | number | symbol

使用 keyof{}unknownundefinednullobject() => void 取值

keyof unknown; // never
keyof undefined; // never
keyof null; // never
keyof object; // never
keyof {}; // never
keyof (() => void); // never

使用 keyofstring 取值

keyof string;
// number | typeof Symbol.iterator | "toString" | "valueOf" | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" | "localeCompare" | "match" | "replace" | "search" | "slice" | "split" | "substring" | "toLowerCase" | "toLocaleLowerCase" | "toUpperCase" | "toLocaleUpperCase" | "trim" | "length" | "substr" | "codePointAt" | "includes" | "endsWith" | "normalize" | "repeat" | "startsWith" | "anchor" | "big" | "blink" | "bold" | "fixed" | "fontcolor" | "fontsize" | "italics" | "link" | "small" | "strike" | "sub" | "sup"

使用 keyofnumber 取值

keyof number; // "toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"

使用 keyofsymbol 取值

keyof symbol; // "toString" | "valueOf" | typeof Symbol.toPrimitive | typeof Symbol.toStringTag

使用 keyofboolean 取值

keyof boolean; // "valueOf"

8. GetOptional

题目

题目链接:GetOptional

实现 GetOptional,返回所有的可选属性

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<GetOptional<{ foo: number; bar?: string }>, { bar?: string }>>,
  Expect<
    Equal<GetOptional<{ foo: undefined; bar?: undefined }>, { bar?: undefined }>
  >
];

答案

这题和 [GetRequired](https://github.com/astak16/blog-ts-challenges/issues/12) 类似

方法一

type GetOptional<T> = {
  [key in keyof T as {} extends Pick<T, key> ? key : never]: T[key];
};

知识点:

利用 空对象 = 纯可选{} extends Pick<T, key>truekey 为可选

方法二

type GetOptional<T> = Pick<
  T,
  { [key in keyof T]: {} extends Pick<T, key> ? key : never }[keyof T]
>;

知识点

思路和方法一是一样的

方法三

type GetOptional<T> = {
  [key in keyof T as T[key] extends Required<T>[key] ? never : key]: T[key];
};

type GetOptional<T, P extends T = Required<T>> = {
  [key in keyof T as T[key] extends P[key] ? never : key]: T[key];
};

type MyRequired<T> = { [K in keyof T]-?: T[K] };
type GetOptional<T> = {
  [key in keyof T as T[key] extends MyRequired<T>[key] ? never : key]: T[key];
};

知识点

利用 Required 将可选变为必选, T[key] extends Required<T>[key]falsekey 为可选。

可以自己实现一个 MyRequried

方法四

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

type FilterKeys<T> = {
  [K in keyof T]: undefined extends T[K] ? K : never;
}[keyof T];

type GetOptional<T> = {
  [K in FilterKeys<MapToKeys<T>>]?: T[K];
};

方法五

type GetOptional<T> = Omit<T, keyof GetRequired<T>>;

知识点

利用 GetRequired 获取所有必选的 key ,使用 Omit 剔除掉。

方法六

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

知识点

在一个对象中剔除掉一个可选的属性,还是属于这个对象

  • Omit<T, K> 其中 K 是可选属性,所以 Omit<T, K> extends Ttrue

方法七

type FilterOptionalKey<T, K extends keyof T> = T extends {
  [k in K]-?: T[k];
}
  ? never
  : K;
type GetOptional<T> = {
  [K in keyof T as FilterOptionalKey<T, K>]: T[K];
};

知识点

T extends {[k in K]-?: T[k]} ,其中 K extends keyof T

  • 假如 k 是可选项, T extends {[k in K]-?: T[k]}false
  • 假如 k 是必选项, T extends {[k in K]-?: T[k]}true

总结

  • {} extends Pick<T, K>trueK 是必选属性
  • Omit<T, K> extends TtrueK 是可选属性

25. TrimLeft、Trim、TrimRight

题目

题目链接:TrimLeftTrimTrimRight

实现 TrimLeft,满足以下要求

import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<TrimLeft<'str'>, 'str'>>,
  Expect<Equal<TrimLeft<' str'>, 'str'>>,
  Expect<Equal<TrimLeft<'     str'>, 'str'>>,
  Expect<Equal<TrimLeft<'     str     '>, 'str     '>>,
  Expect<Equal<TrimLeft<'   \n\t foo bar '>, 'foo bar '>>,
  Expect<Equal<TrimLeft<''>, ''>>,
  Expect<Equal<TrimLeft<' \n\t'>, ''>>,
]

答案

方法一

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

方法二

type TrimLeft<T extends string> = T extends ` ${infer R}`
  ? TrimLeft<R>
  : T extends `\t${infer R}`
  ? TrimLeft<R>
  : T extends `\n${infer R}`
  ? TrimLeft<R>
  : T;

实现 Trim,TrimRight

Trim

type Space = `${" " | "\t" | "\n"}`;
type Trim<T extends string> = T extends
  | `${Space}${infer R}`
  | `${infer R}${Space}`
  ? Trim<R>
  : T;

TrimRight

type Space = `${" " | "\t" | "\n"}`;
type TrimRight<T extends string> = T extends `${infer R}${Space}`
  ? TrimRight<R>
  : T;

10. If

题目

题目链接:If

实现 If,接收三个参数,第一个参数是条件,只能够是 boolean 类型,后面两个参数为任意类型,如果第一个参数为 true 则返回第二个参数,如果第一个参数为 false ,则返回第三个参数。

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<If<true, "a", "b">, "a">>,
  Expect<Equal<If<false, "a", 2>, 2>>
];

// @ts-expect-error
type error = If<null, "a", "b">;

答案

方法一

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

知识点

这题有个 ts-expect-error ,所以第一个参数需要约束,只能传入 boolean 类型。

20. Chainable

题目

题目链接:ChainableOption回答链接

实现 Chainable ,满足下面要求

import type { Alike, Expect } from "@type-challenges/utils";

declare const a: Chainable;

const result1 = a
  .option("foo", 123)
  .option("bar", { value: "Hello World" })
  .option("name", "type-challenges")
  .get();

const result2 = a
  .option("name", "another name")
  // @ts-expect-error
  .option("name", "last name")
  .get();

type cases = [
  Expect<Alike<typeof result1, Expected1>>,
  Expect<Alike<typeof result2, Expected2>>
];

type Expected1 = {
  foo: number;
  bar: {
    value: string;
  };
  name: string;
};

type Expected2 = {
  name: string;
};

答案

方法一

type Chainable<T = unknown> = {
  option<K extends string, V>(
    key: K extends keyof T ? never : K,
    value: V
  ): Chainable<T & { [k in K]: V }>;
  get(): T;
};

知识点

用一个例子来讲解

declare const a: Chainable;
const result1 = a
  .option("foo", 123)
  .option("bar", { value: "Hello World" })
  .get();

type Result1 = {
  foo: number;
};
  1. 第一个 option 函数内部推导

    1. T = unknown, keyof T = never, K = "foo", V = 123
    2. key = "foo" extends never => false => "foo", value = 123
    3. T & {[k in K]: V} = unknown & {foo: 123} = {foo: 123}
       Chainable<{foo: 123}>
       T = {foo: 123}
  2. 第二个 option 函数内部推导

    // T 是第一个 option 函数的结果
    1. T = {foo: 123}, keyof T = "foo", K = "bar", V = { value: "Hello World" }
    2. key = "bar" extends "foo" => false => "bar", value = { value: "Hello World" }
    3. T & {[k in K]: V} = {foo: 123} & {bar: { value: "Hello World" }}
       Chainable<{foo: 123} & {bar: { value: "Hello World" }}>
       T = {foo: 123} & {bar: { value: "Hello World" }}
  3. 调用 get 得到结果,返回 T

如果调用 option 传入相同的 key 时,应该要报错,看下面的例子

const result2 = a
  .option("name", "another name")
  .option("name", "last name")
  .get();

type Result2 = {
  name: string;
};
  1. 第一个 option 推导出的结果是 {name: "another name"}

  2. 第二个 optionkey 得到的值为 never

    K extends keyof T ? never : K => "name" extends "name" => true => K = never
    Chainable<{name: "another name"}>
    T = {name: "another name"}
  3. 调用 get 得到结果,返回 T

方法二

type Chainable<T = unknown> = {
  option<K extends string, V>(
    key: K extends keyof T ? never : K,
    value: V
  ): Chainable<{ [P in K | keyof T]: P extends keyof T ? T[P] : V }>;
  get(): T;
};

知识点

用例子来讲解一步步的推算过程

declare const a: Chainable;
const result1 = a
  .option("foo", 123)
  .option("bar", { value: "Hello World" })
  .get();

type Result1 = {
  foo: number;
};
  1. 第一个 option 函数内部推导

    1. T = unknown, keyof T = never, K = "foo", V = 123
    2. key = "foo" extends never => false => "foo", value = 123
    3. 对象左边 = K | keyof T => "foo" | never => P = "foo"
       对象右边 = "foo" extends never => false => 123
       Chainable<{foo: 123}>
       T = {foo: 123}
  2. 第二个 option 函数内部推导

    // T 是第一个 option 函数的结果
    1. T = {foo: 123}, keyof T = "foo", K = "bar", V = { value: "Hello World" }
    2. key = "bar" extends "foo" => false => "bar", value = { value: "Hello World" }
    3. 对象左边 = K | keyof T => "bar" | "foo" => P = "bar" 或者 "foo"
       对象右边 =
        P = "bar" 时,"bar" extends "foo" =? false => { value: "Hello World" }
        P = "foo" 时,"foo" extends "foo" =? true => 123
       Chainable<{bar: { value: "Hello World" }; foo: 123}>
       T = {bar: { value: "Hello World" }; foo: 123}
  3. 调用 get 得到结果,返回 T

如果调用 option 传入相同的 key 时,应该要报错,看下面的例子

const result2 = a
  .option("name", "another name")
  .option("name", "last name")
  .get();

type Result2 = {
  name: string;
};
  1. 第一个 option 推导出的结果是 {name: "another name"}

  2. 第二个 optionkey 得到的值为 never

    K extends keyof T ? never : K => "name" extends "name" => true => K = never
    Chainable<{name: "another name"}>
    T = {name: "another name"}
  3. 调用 get 得到结果,返回 T

方法三

type Chainable = {
  option<T, K extends string, V>(
    this: T,
    key: K extends keyof T ? never : K,
    value: V
  ): T & { [key in K]: V };
  get<T>(this: T): Omit<T, "option" | "get">;
};

知识点

这里的 this 指代 Chainable 本身,由于它本身包含了 optionget 两个方法,所以要用 Omit 把这两个方法去除掉。

TIP

  1. { [k in K]: V } 可以用 Record<K, V> 代替
  2. K extends keyof T ? never : K 可以用 Exclude<K, keyof T> 代替

12. MyReadonly2

题目

题目链接:MyReadonly2

实现 MyReadonly2 ,接收两个参数,把第二个参数变为只读,同时第二个参数可以不传,不传的话就把第一个参数中所有的属性都变位只读。

import type { Alike, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>,
  Expect<Alike<MyReadonly2<Todo1, "title" | "description">, Expected>>,
  Expect<Alike<MyReadonly2<Todo2, "title" | "description">, Expected>>
];

interface Todo1 {
  title: string;
  description?: string;
  completed: boolean;
}

interface Todo2 {
  readonly title: string;
  description?: string;
  completed: boolean;
}

interface Expected {
  readonly title: string;
  readonly description?: string;
  completed: boolean;
}

答案

实现 MyReadonly2 类型,把 T 变位只读,用 OmitT 中挑选出 K,最后使用 & 连接

方法一

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [key in K]: T[key];
} & Omit<T, K>;

方法二

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [key in K]: T[key];
} & {
  [key in K as key extends K ? key : never]: T[key];
};

方法三

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

27. Replace And ReplaceAll

题目

题目链接:Replace

实现 Replace,满足下面需求

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<Replace<"foobar", "bar", "foo">, "foofoo">>,
  Expect<Equal<Replace<"foobarbar", "bar", "foo">, "foofoobar">>,
  Expect<Equal<Replace<"foobarbar", "", "foo">, "foobarbar">>,
  Expect<Equal<Replace<"foobarbar", "bar", "">, "foobar">>,
  Expect<Equal<Replace<"foobarbar", "bra", "foo">, "foobarbar">>,
  Expect<Equal<Replace<"", "", "">, "">>
];

答案

方法一

type Replace<
  R extends string,
  From extends string,
  To extends string
> = From extends ""
  ? R
  : R extends `${infer A}${From}${infer B}`
  ? `${A}${To}${B}`
  : R;

知识点

要想在 R 中拿到 From,可以把 R 拆分为 ${infer A}${From}${infer B},然后把 From 替换为 To就可以了。

题目2

题目链接:ReplaceAll

实现 ReplaceAll,满足下面需求

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<ReplaceAll<"foobar", "bar", "foo">, "foofoo">>,
  Expect<Equal<ReplaceAll<"foobar", "bag", "foo">, "foobar">>,
  Expect<Equal<ReplaceAll<"foobarbar", "bar", "foo">, "foofoofoo">>,
  Expect<Equal<ReplaceAll<"t y p e s", " ", "">, "types">>,
  Expect<Equal<ReplaceAll<"foobarbar", "", "foo">, "foobarbar">>,
  Expect<Equal<ReplaceAll<"barfoo", "bar", "foo">, "foofoo">>,
  Expect<Equal<ReplaceAll<"foobarfoobar", "ob", "b">, "fobarfobar">>,
  Expect<Equal<ReplaceAll<"foboorfoboar", "bo", "b">, "foborfobar">>,
  Expect<Equal<ReplaceAll<"", "", "">, "">>
];

答案

方法一

type ReplaceAll<
  R extends string,
  From extends string,
  To extends string
> = From extends ""
  ? R
  : R extends `${infer A}${From}${infer B}`
  ? `${A}${To}${ReplaceAll<B, From, To>}`
  : R;

知识点

Replace 思路是一样的,只是 Replace 只把第一个 From 替换成 ToReplaceAll 需要把所有的 From 都替换成 To,所有就需要递归去找 From

11. Concat

题目

题目链接:Concat

实现 Concat ,它接收两个参数,两个参数的类型都是数组,将两个数组进行连接,并返回它。

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<Concat<[], []>, []>>,
  Expect<Equal<Concat<[], [1]>, [1]>>,
  Expect<Equal<Concat<[1, 2], [3, 4]>, [1, 2, 3, 4]>>,
  Expect<
    Equal<
      Concat<["1", 2, "3"], [false, boolean, "4"]>,
      ["1", 2, "3", false, boolean, "4"]
    >
  >
];

答案

方法一

type Concat<T extends readonly unknown[], F extends readonly unknown[]> = [
  ...T,
  ...U
];

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.