Ts 类型体操专项训练 Ⅱ

374 阅读8分钟

本文对 11 个Ts 类型体操进行专项训练,喜欢的朋友可以收藏起来今后翻看。

1. Anyof

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

例如:

type Sample1 = AnyOf<[1, '', false, [], {}]>; // expected to be true.
type Sample2 = AnyOf<[0, '', false, [], {}]>; // expected to be false.

自写答案

type AnyOf<T extends any[]> = T extends [infer A, ...infer B] ? (
  A extends any[] ? (
    A["length"] extends 0 ? AnyOf<B> : true
  ) : (A extends object ? (
    [keyof A] extends [never] ? AnyOf<B> : true
  ): (A extends (0|""|false|null|undefined|[]) ? AnyOf<B> : true))
) : false;

答案

type AnyOf<T extends readonly any[]> = T[number] extends
    | ''
    | 0
    | []
    | false
    | { [P in any]: never }
    ? false
    : true;

使用 { [P in any]: never } 表示 {}, 使用 object 表示一般的对象,但是 object 包含所有数组。

不得不说,答案还是巧妙!

2. NumberRange

有时我们需要限制数字的范围...

示例:

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

自写答案

type BuildArray<T extends number, K extends unknown[] = []> = {
  0: K,
  1: BuildArray<T, [...K, unknown]>
}[
  K["length"] extends T ? 0 : 1
]


type NumberRange<A extends number, B extends number, C extends unknown[] = BuildArray<A>, D extends any[] = []> = {
  0: D[number],
  1: NumberRange<A, B, [...C, unknown], [...D, C["length"]]>
}[
  C["length"] extends [...BuildArray<B>, unknown]["length"] ? 0 : 1
]

答案

type NumberRange<
  L extends number,
  H extends number,
  CArr extends any[] = [],
  OArr extends unknown[] = [unknown],
  R extends number = H
> = H extends CArr['length']
  ? R
  : L extends CArr['length']
  ? NumberRange<OArr['length'], H, [any, ...CArr], [unknown, ...OArr], L | R>
  : NumberRange<L, H, [...CArr, any], [unknown, ...OArr]>;

下面的这两种写法,后面的可读性强一些,但是安全性不如前面那个:


type BuildArray2<T extends number, K extends any[] = []> = {
  0: K,
  1: BuildArray<T, [any, ...K]>
}[
  K["length"] extends T ? 0 : 1
]

type BuildArray<T extends number, K extends any[] = []> = K["length"] extends T ? K : BuildArray<T, [any, ...K]>;

3. ConstructTuple

构造一个给定长度的元组。

例如

type result = ConstructTuple<2> // 期望得到 [unknown, unkonwn]

自写答案

type ConstructTuple<T extends number, K extends any[] = []> = K["length"] extends T ? K : BuildArray<T, [any, ...K]>;

答案

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

4. MapTypes

实现 MapTypes<T, R>, 它将对象 T 中的类型转换为有如下结构的类型 R 定义的类型

type StringToNumber = {
    mapFrom: string; // 类型为 string 的键(key)
    mapTo: number; // 会被转换为 number
};

MapTypes<{ iWillBeANumberOneDay: string }, StringToNumber>; // 结果 { iWillBeANumberOneDay: number; }

自写答案

type MapTypes<T extends object, K extends {
  mapFrom: any;
  mapTo: any;
}> = {
  [P in keyof T] : T[P] extends K["mapFrom"] ? K["mapTo"] : T[P]
}

答案

type MapTypes<T, R extends { mapFrom: unknown; mapTo: unknown }> = {
    [P in keyof T]: T[P] extends R['mapFrom']
        ? R extends R
            ? Equal<T[P], R['mapFrom']> extends true
                ? R['mapTo']
                : never
            : never
        : T[P];
};

为什么包了两层?

R extends R

Equal<T[P], R['mapFrom']> extends true

推测可能是第二种写法的 T 没有限制为 object.

5. Unique

实现类型的 Lodash.uniq, Unique 接受数组 T, 返回没有重复值的数组 T

type Res = Unique<[1, 1, 2, 2, 3, 3]>; // expected to be [1, 2, 3]
type Res1 = Unique<[1, 2, 3, 4, 4, 5, 6, 7]>; // expected to be [1, 2, 3, 4, 5, 6, 7]
type Res2 = Unique<[1, 'a', 2, 'b', 2, 'a']>; // expected to be [1, "a", 2, "b"]
type Res3 = Unique<[string, number, 1, 'a', 1, string, 2, 'b', 2, number]>; // expected to be [string, number, 1, "a", 2, "b"]
type Res4 = Unique<[unknown, unknown, any, any, never, never]>; // expected to be [unknown, any, never]

自写答案

type IsIn<T, K extends any[] = []> = K extends [infer A, ...infer B] ? (
  (<F>() => F extends A ? 1 : 0) extends (<F>() => F extends T ? 1 : 0) ? true : IsIn<T, B>
) : false;

type Unique<T extends any[], K extends any[] = []> = T extends [infer A, ...infer B] ? (
  IsIn<A, K> extends true ? Unique<B, K> : Unique<B, [...K, A]>
) : K;

答案

type Contains<TArr extends any[], T> = TArr extends [infer L, ...infer R]
    ? (<F>() => F extends L ? 1 : 0) extends <F>() => F extends T ? 1 : 0
        ? true
        : Contains<R, T>
    : false;

type Unique<T extends any[], O extends any[] = []> = T extends [
    infer L,
    ...infer R
]
    ? Contains<O, L> extends true
        ? Unique<R, O>
        : [L, ...Unique<R, [...O, L]>]
    : [];

这个题的难点在于如何证明两个类型是严格相等的,也就是不仅能够判断出 'a' 和 'a' 相等,还要判断出 'a' 和 string 不相等,这里提供一种借用函数的方式:

type IsEqual<A, B> = (<F>() => F extends A ? 1 : 0) extends (<F>() => F extends B ? 1 : 0) ? true : false;

我们将其稍作改动即可得到能够判断 never 的 IsNever 或者 IsUnknown 或者其他。

type IsEqual<T> = Equal<T, never>

6. LastIndexOf

实现类型的 Array.lastIndexOf, LastIndexOf<T, U> 接受泛型参数 Array T 和 any U 并返回数组 T 中最后一个 U 的索引

示例:

type Res1 = LastIndexOf<[1, 2, 3, 2, 1], 2>; // 3
type Res2 = LastIndexOf<[0, 0, 0], 2>; // -1

自写答案

type IsEqual<A, B> = (<F>() => F extends A ? 1 : 0) extends (<F>() => F extends B ? 1 : 0) ? true : false;

type LastIndexOf<T extends any[], K> = T extends [...infer A, infer B] ? (
  IsEqual<B, K> extends true ? A["length"] : LastIndexOf<A, K>
) : -1;

答案

type LastIndexOf<T extends any[], U> = T extends [...infer L, infer R]
    ? (<F>() => F extends R ? 1 : 0) extends <F>() => F extends U ? 1 : 0
        ? L['length']
        : LastIndexOf<L, U>
    : -1;

IsEqual 简直神器。

7. Join

实现类型版本的 Array.join, Join<T, U> 接受数组 T 和 string 或者 number 类型 U 作为泛型参数, 并返回 U 连接数组 T 后的字符串.

自写答案

type CanInTemplate = string | number | bigint | boolean | null | undefined;

type Join<T extends unknown[], U extends string | number, C extends string = ''> = T extends [infer A, ...infer B] ? (
  A extends CanInTemplate ? Join<B, U, `${C}${U}${A}`> : C extends `${U}${infer Re}` ? Re : never
) : (C extends `${U}${infer Re}` ? Re : never)

答案

type Stringify = string | number | bigint | boolean | null | undefined;
type Join<
    T extends Stringify[],
    U extends Stringify,
    S extends Stringify = ''
> = T extends [infer L extends string, ...infer R extends string[]]
    ? Join<R, U, '' extends S ? `${L}` : `${S}${U}${L}`>
    : S;
  1. 模板字符串允许的类型有 string | number | bigint | boolean | null | undefined;
  2. infer 之后还可以使用断言 T extends [infer L extends string, ...infer R extends string[]]

8. IndexOf

实现类型版本的 Array.indexOf, indexOf<T, U> 接受一个数组T和any类型的U作为参数, 返回T中第一个U的索引

type Res = IndexOf<[1, 2, 3], 2>; // expected to be 1
type Res1 = IndexOf<[2, 6, 3, 8, 4, 1, 7, 3, 9], 3>; // expected to be 2
type Res2 = IndexOf<[0, 0, 0], 2>; // expected to be -1

自写答案

type IsEqual<A, B> = (<F>()=>F extends A ? 0 : 1) extends (<F>()=>F extends B ? 0 : 1) ? true : false;

type IndexOf<T extends any[], R , C extends any[] = []> = T extends [infer A, ...infer B] ? (
  IsEqual<A, R> extends true ? C["length"] : IndexOf<B, R, [...C, A]>
) : -1;

答案

type IndexOf<T extends any[], U extends any, S extends any[] = []> = T extends [
    infer L,
    ...infer R
]
    ? true extends IsEqual<L, U>
        ? S['length']
        : IndexOf<R, U, [...S, 0]>
    : -1;

9. Trunc

实现类型版本的 Math.trunc, 其接受一个字符串或数字作为泛型参数, 并返回移除了全部小数位部分后的整数。

示例:

type A = Trunc<12.34>; // 12

自写答案

type Trunc<T extends number> = `${T}` extends `${infer R}.${any}` ? R : `${T}`;

答案

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

这答案没有我写的利索。

10. Without

实现一个像 Lodash.without 函数一样的泛型 Without<T, U>,它接收数组类型的 T 和数字或数组类型的 U 为参数,会返回一个去除 U 中元素的数组 T。

例如:

type Res = Without<[1, 2], 1>; // expected to be [2]
type Res1 = Without<[1, 2, 4, 1, 5], [1, 2]>; // expected to be [4, 5]
type Res2 = Without<[2, 3, 2, 3, 2, 3, 2, 3], [2, 3]>; // expected to be []

自写答案

type IsEqual<A, B> = (<F>() => F extends A ? 1 : 0) extends <F>() => F extends B
  ? 1
  : 0
  ? true
  : false;

type _Without<T extends any[], U, K extends any[] = []> = T extends [
  infer A,
  ...infer B
]
  ? IsEqual<A, U> extends true
    ? _Without<B, U, [...K]>
    : _Without<B, U, [...K, A]>
  : K;

type Without<
  T extends any[],
  U extends any[]|any,
  K extends any[] = []
> = U extends any[] ? (
  U extends [infer A, ...infer B] ? Without<_Without<T, A, K>, B, K> : T
) : _Without<T, U, K>

答案

type Without<T extends any[], U extends any[] | number> = U extends number
    ? Without<T, [U]>
    : T extends [infer L, ...infer R]
    ? [
          ...(U extends [infer UL, ...infer UR]
              ? L extends UL
                  ? []
                  : Without<[L], UR>
              : [L]),
          ...Without<R, U>
      ]
    : [];

感觉对于数组遍历的出口不是很清楚,所以写不出双循环。

11. TrimRight

实现 TrimRight<T> ,它接收确定的字符串类型并返回一个新的字符串,其中新返回的字符串删除了原字符串结尾的空白字符串。

例如

type Trimed = TrimRight<'  Hello World  '>; // 应推导出 '  Hello World'

自写答案

type TrimLeft<T extends string> = T extends `${infer A}${infer B}` ? (
  A extends " "|"\r"|"\n" ? TrimLeft<B> : T
) : '';

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

答案

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

从左向右遍历容易,反过来难。

12. Fill

Fill 是 javascript 中常用的方法, 现在让我实现类型版本的 Fill Fill<T, N, Start?, End?>, 正如你看到的那样, Fill接受四个泛型参数, 其中 T 和 N 是必填参数, Start 和 End 是可选参数 这些参数的要求如下: T 必须是一个元组(tuple), N 可以是任何值类型, Start 和 End 必须是大于或等于 0 的整数

自写答案

type BuildArray<T extends number, K extends unknown[] = []> = K["length"] extends T ? K : BuildArray<T, [...K, unknown]>;

type NumberRange<A extends number, B extends number, C extends unknown[] = BuildArray<A>, D extends any[] = []> = {
  0: D[number],
  1: NumberRange<A, B, [...C, unknown], [...D, C["length"]]>
}[
  C["length"] extends [...BuildArray<B>, unknown]["length"] ? 0 : 1
]

type Fill<T extends any[], N, Start extends number = 0, End extends number = T["length"], Re extends any[] = []> = T extends [infer A, ...infer B] ?  (
  Fill<B, N, Start, End, Re["length"] extends NumberRange<Start, End> ? [...Re, N] : [...Re, A]>
) : Re;

答案

type Fill<
    T extends unknown[],
    N,
    Start extends number = 0,
    End extends number = T['length'],
    Pending extends any[] = [],
    Filled extends any[] = []
> = T extends [infer L, ...infer R]
    ? [
          Pending['length'] extends Start
              ? Filled['length'] extends End
                  ? L
                  : N
              : L,
          ...Fill<
              R,
              N,
              Start,
              End,
              Pending['length'] extends Start ? Pending : [...Pending, 0],
              Filled['length'] extends End ? Filled : [0, ...Filled]
          >
      ]
    : [];

我觉得有一些经常用的,可以背下来比如:NumberRange 或者 BuildArray.

13. Chunk

你知道 lodash 吗? lodash中有一个非常实用的方法Chunk, 让我们实现它吧. Chunk<T, N>接受两个泛型参数, 其中 T 是 tuple 类型, N是大于 1 的数字

type exp1 = Chunk<[1, 2, 3], 2>; // expected to be [[1, 2], [3]]
type exp2 = Chunk<[1, 2, 3], 4>; // expected to be [[1, 2, 3]]
type exp3 = Chunk<[1, 2, 3], 1>; // expected to be [[1], [2], [3]]

自写答案

思路就是找一个池子往里面添加元素,满了之后以独立包装的形式添加到大池子中去。

type Chunk<T extends any[], K extends number, Cur extends any[] = [], Re extends any[] = []> = T extends [infer A, ...infer B] ? (
    Cur['length'] extends K ? (
        Chunk<T, K, [], [...Re, Cur]>
    ) : Chunk<B, K, [...Cur, A], [...Re]>
) : [...Re, Cur];

答案

type Chunk<
    T extends any[],
    N extends number,
    S extends any[] = [],
    ALL extends any[] = []
> = T extends [infer L, ...infer R]
    ? S['length'] extends N
        ? Chunk<T, N, [], [...ALL, S]>
        : Chunk<R, N, [...S, L], ALL>
    : S['length'] extends 0
    ? []
    : [...ALL, S];

只能说一模一样。

14. IsTuple

实现 IsTuple 类型, 接受一个泛型参数 T 作为输入, 并返回 T 是否为 tuple 类型

示例:

type case1 = IsTuple<[number]>; // true
type case2 = IsTuple<readonly [number]>; // true
type case3 = IsTuple<number[]>; // false

自写答案

type IsTuple<T> = T extends (any[]|readonly any[]) ? (
  T["length"] extends number ? (
    number extends T["length"] ? false : true
  ) : false
) : false;

答案

/**
 * 元组的长度是有限的,其`length`属性返回的是数字字面量;数组的`length`属性的类型是`number`
 */
type IsTuple<T> = [T] extends [never]
    ? false
    : T extends readonly any[]
    ? number extends T['length']
        ? false
        : true
    : false;

注意:

  1. readonly [number] extends readonly any[] 为真; readonly [number] extends any[] 为假。
  2. 由于 Ts 中没有特定义类型 Tuple 故不能使用 IsEqual.
  3. number[]["length"] 的值为 number.
  4. number extends number 为真,但是 number extends 1 为假。

15. GreaterThan

在本挑战中, 你需要实现 GreaterThan<T, U>, 它的作用像 T > U

你不需要考虑负数

示例:

GreaterThan < 2,
    1 > //should be true
        GreaterThan <
        1,
    1 > //should be false
        GreaterThan <
        10,
    100 > //should be false
        GreaterThan<111, 11>; //should be true

自写答案

type BuildTuple<T extends number, K extends any[] = []> = K["length"] extends T ? K : BuildTuple<T, [...K, unknown]>

type MinusOne<T extends number> = BuildTuple<T> extends [infer A, ...infer B] ? B["length"] : -1;

type GreaterThan<A extends number, B extends number> = MinusOne<A> extends 0 ? (
  MinusOne<B> extends 0 ? false: false
) : (
  MinusOne<B> extends 0 ? true: GreaterThan<MinusOne<A>, MinusOne<B>>
)

答案

type GreaterThan<
    T extends number,
    U extends number,
    A extends any[] = []
> = A['length'] extends T
    ? false
    : A['length'] extends U
    ? true
    : GreaterThan<T, U, [...A, 0]>;

答案更加巧妙和简洁哦。

16. Flip

实现一个 just-flip-object 类型

示例:

Flip<{ a: 'x'; b: 'y'; c: 'z' }>; // {x: 'a', y: 'b', z: 'c'}
Flip<{ a: 1; b: 2; c: 3 }>; // {1: 'a', 2: 'b', 3: 'c'}
Flip<{ a: false; b: true }>; // {false: 'a', true: 'b'}

自写答案

type Flip<T extends Record<PropertyKey, string | number | bigint | boolean | null | undefined>> = {
  [P in keyof T as `${T[P]}`]: P
}

答案

type Flip<T> = {
    [K in keyof T as T[K] extends
        | string
        | number
        | bigint
        | boolean
        | null
        | undefined
        ? `${T[K]}`
        : never]: K;
};

使用 as 则之前的任何内容都只有形式上的作用。

17. Fibonacci

实现一个输入参数为数字 T 的泛型类型 Fibonacci<T>, 返回 T 对应的斐波那契数

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

自写答案

type BuildTuple<T extends number, K extends any[] = []> = K["length"] extends T ? K : BuildTuple<T, [...K, unknown]>;

type Add<T extends number, K extends number> = [...BuildTuple<T>, ...BuildTuple<K>]["length"];

type MinusOne<T extends number> = BuildTuple<T> extends [infer A, ...infer B] ? B["length"] : -1;

type Fibonacci<T extends number> = T extends 1 ? 1 : (
  T extends 2 ? 1 : (
    Add< Fibonacci<MinusOne<T>>, Fibonacci<MinusOne<MinusOne<T>>>>
  )
)

答案

type Fibonacci<
    T extends number,
    ACC extends [any[], any[], any[]] = [[any], [any], [any, any]]
> = T extends 1 | 2
    ? 1
    : ACC[2]['length'] extends T
    ? ACC[0]['length']
    : Fibonacci<T, [[...ACC[0], ...ACC[1]], [...ACC[0]], [any, ...ACC[2]]]>;

结合第 15 题,需要提升对数组的使用技巧。

18. InorderTraversal

实现二叉树的中序遍历

示例:

const tree1 = {
    val: 1,
    left: null,
    right: {
        val: 2,
        left: {
            val: 3,
            left: null,
            right: null,
        },
        right: null,
    },
} as const;
type A = InorderTraversal<typeof tree1>; // [1, 3, 2]

自写答案

type InorderTraversal<T extends TreeNode, R extends number[] = []> = T["left"] extends TreeNode ? (
  T["right"] extends TreeNode ? (
    [...InorderTraversal<T["left"], R>, T["val"], ...InorderTraversal<T["right"], R>]
  ) : [...InorderTraversal<T["left"], R>, T["val"]]
) : (
  T["right"] extends TreeNode ? (
    [...R, T["val"], ...InorderTraversal<T["right"], R>]
  ) : [...R, T["val"]]
)

答案

interface TreeNode {
    val: number;
    left: TreeNode | null;
    right: TreeNode | null;
}
type InorderTraversal<T extends TreeNode | null> = T extends TreeNode
    ? [
          ...(T['left'] extends TreeNode ? InorderTraversal<T['left']> : []),
          T['val'],
          ...(T['right'] extends TreeNode ? InorderTraversal<T['right']> : [])
      ]
    : [];

注意定义 TreeNode 的时候可以立即开始递归。

19. BEM style string

Block,Element,Modifier 方法(BEM)是 CSS 中类的流行命名约定。

例如,块组件用 btn 表示,依赖于块的元素用 btn__price 表示,更改块样式的修饰符将用 btn--big 或者 btn__price--warning表示。

实现从这三个参数生成字符串联合的 BEM<B, E, M>。其中 B 是字符串字面量,E 和 M 是字符串数组(可以为空)。

自写答案

没看懂题目让干啥,直接抄答案好了。

答案

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

这道题大概是想说在 `${}` 中也可以使用三目运算符吧!