更强大的使用TypeScript(5)

126 阅读4分钟
    /**
        @date 2021-03-20
        @description ts-type-challenge-困难题目(5)
    */

壹(序)

挑战hard难度的type-challenge,其实感觉还好,和一些medium差不多,不过也存在难度较大的(medium也有难度超标的),最大的感受就是会用到很多之前做过的题里面的知识,某道题用到了以前的哪些题目的想法或者整个都使用的,会在题解里面说明,所以感觉hard更像是做完easy与medium的一次检验;

贰(Currying)

首先需要实现一个Curry,传入arguments以及result,输出一个Curry后的函数,Curry很简单,使用infer以及递归就可以实现; 然后就是得到Currying中的arguments以及result,可以定义一个F,就是Currying传入的fn函数,然后使用infer推导出arguments以及result,在调用Curry即可:

type Curry<A, R> = A extends [infer F, ...infer L] ? (arg: F) => Curry<L, R> : R

declare function Currying<F>(fn: F): F extends (...args: infer A) => infer R ? Curry<A, R> : never

叁(UnionToIntersection)

这道题主要是需要明白:

type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }

? U

: never;

type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string

type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number

这段代码来自 官方文档;不过在大概3.6版本之后,这里的T21就会得到never,这很好理解,一个类型不可能满足既是string又是number,所以 string & number extends never ? true : false会得到 true;

所以这道题我们只需要构建一个函数去推导就行了:

type UnionToIntersection<U> = (U extends never ? never : (a: U) => void) extends (a: infer A) => void ? A : never;

肆(GetRequired)

这种需要去除object中某些项的题目之前遇到过,使用as never即可,所以这里直接遍历每一个K,将不满足条件的去除就行,那么哪些是不满足条件的呢,我们可以使用Required来判断:

type GetRequired<T> = {
  [K in keyof T as (T[K] extends Required<T>[K] ? K : never)]: T[K]
}

伍(GetOptional)

GetRequired不能说毫无相关,只能说一模一样:

type GetOptional<T> = {
  [K in keyof T as (T[K] extends Required<T>[K] ? never : K)]: T[K]
}

陆(RequiredKeys)

GetRequired的基础上使用keyof得到对象的keys即可:

type RequiredKeys<T> = keyof {
  [K in keyof T as (T[K] extends Required<T>[K] ? K : never)]: T[K]
}

柒(OptionalKeys)

:D

type OptionalKeys<T> = keyof {
  [K in keyof T as (T[K] extends Required<T>[K] ? never : K)]: T[K]
}

捌(CapitalizeWords)

Capitalize的基础上,需要将每个单词首字母都大写,不仅是以' '隔开的,还要处理以',''.'隔开的;

首先这种题肯定是需要递归的,一项一项的去处理,在遇到上面三种间隔符('', ',', '.')时就递归处理,当然,即使不是以上间隔符,也需要递归,最后跳出递归条件就是第一个CapitalizeWords遍历完成;

不过需要注意一点的是,第一个S的首字母没有条件调用Capitalize,所以一开始就需要处理一下:

type CapitalizeWords<S extends string, T = Capitalize<S>, R extends string = ''> =
  T extends `${infer F}${infer L}`
  ? F extends TestStrs
  ? CapitalizeWords<L, Capitalize<L>, `${R}${F}`>
  : CapitalizeWords<L, L, `${R}${F}`>
  : R

玖(CamelCase)

与上面的CapitalizeWords类似,只是递归传值的时候需要注意CapitalizeWords是会带上之前的间隔符的,但是这里不需要:

type CamelCase<S extends string, T = Lowercase<S>, R extends string = ''> =
  T extends `${infer F}${infer L}`
  ? F extends '_'
  ? CamelCase<Capitalize<L>, Capitalize<L>, `${R}`>
  : CamelCase<L, L, `${R}${F}`>
  : R

拾(ParsePrintFormat)

简单来说这道题就是根据%转义,%与其后面一位构成一组,转义这组数据然后得到字符串里所有可转义的结果数组;

所以首先需要一个转义类型,很简单,使用infer得到第一位再去取ControlsMap中的值即可:

type GetControl<S extends string> =
  S extends `${infer F}${infer L}`
  ? F extends keyof ControlsMap
  ? ControlsMap[F]
  : never
  : never

然后就是写ParsePrintFormat,这里很显然需要递归,所以我们自定义一个R来保存每次遇到%后得到的转义结果,等遍历完字符串所有%时就能跳出递归了:

type ParsePrintFormat<S extends string, R extends any[] = []> =
  S extends `${infer F}%${infer L}`
  ? GetControl<L> extends never
  ? ParsePrintFormat<L extends `${infer LF}${infer LL}` ? LL : L, R>
  : ParsePrintFormat<L, [...R, GetControl<L>]>
  : R

拾壹(IsAny)

type IsAny<T> = 'test' extends true & T ? true : false

拾贰(Get)

根据key获取对象中的值,首先想到需要拆解传入的key,根据'.'来进行拆解成一个key数组,所以先实现一个GetKeysArr

type GetKeysArr<S extends string, R extends string[] = []> = S extends `${infer F}.${infer L}` ? GetKeysArr<L, [...R, F]> : [...R, S]

然后自定义一个R,默认值为never,作为最后的result,再根据keysArr进行遍历,在该array长度为0时跳出遍历返回R,不为0时需要递归,递归的传参前两个不变,依然是T和K;

第三位是keysArr,需要除去第一位;

第四位R,需要重新赋值,这里需要判断R是否为never,如果为never则表示第一次取R,所以从T上取,不为never则直接从R上取最新值;其次需要注意当前key能不能在R中取到,取不到的话需要赋值为never;

最后得到:

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

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

type Get<T extends {
  [key: string]: any
}, K extends string, A extends string[] = GetKeysArr<K>, R = never> =
  A['length'] extends 0
  ? R
  : Get<T, K, Shift<A>, IsNever<R> extends true ? T[A[0]] : A[0] extends keyof R ? R[A[0]] : never>

不过在写到为never则表示第一次取R时想到,如果某个key对应的值就是never呢?那就不对了,所以需要改进,想到的是增加一个boolean的flag,用来表示是否第一次取R,所以得到:

type Get<T extends {
  [key: string]: any
}, K extends string, A extends string[] = GetKeysArr<K>, R = never, IsFirst = true> =
  A['length'] extends 0
  ? R
  : Get<T, K, Shift<A>, IsFirst extends true ? T[A[0]] : A[0] extends keyof R ? R[A[0]] : never, false>

拾叁(ToNumber)

这道题与之前的Length of String几乎一样,只要记住ts类型系统是无法进行数学运算的,所以涉及到数字的话,一般需要自定义一个数组,用此数组的length来表示数字:

type ToNumber<S extends string, R extends any[] = []> = S extends `${R['length']}` ? R['length'] : ToNumber<S, [...R, S]>

拾肆(FilterOut)

思路很简单,遍历数组,一项一项的比较即可,需要注意的是never的情况,在入参的某个范型是never时,TS一定会返回never,这在之前的IsNever中遇到过:

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

拾伍(Enum)

一开始做过tuple转object,这里其实很类似,不过需要加上readonly,其次是第二个参数为true时是需要返回数字,因为是enum转object,所以题目很好理解;

先处理正常的返回key的情况,与TupleToObject基本一样,只是要加上readonly以及key需要首字母大写:

type EnumToObject<T extends readonly any[]> = {
  readonly [K in T[number]as Capitalize<K>]: K
}

然后是处理返回数字的情况,如果从上面看下来应该能知道,遇到需要返回数字,我们应该自定义一个数组,取数组的length去返回,其次这里还需要递归,因为需要一项一项的增加自定义数组的length,才能实现返回值逐步+1

type EnumToObjectNumber<T extends readonly string[], P extends any[] = []> = T extends readonly [infer F, ...infer L] ? {
  readonly [K in Capitalize<F extends string ? F : never>]: P['length']
} & TupleToObject2<L extends readonly string[] ? L : [], [...P, F]> : {}

但是EnumToObjectNumber得到的是由&链接的对象,所以简单写一个Merge

type MergeObject<T extends { [key: string]: any }> = {
  [K in keyof T]: T[K]
}

最后根据第二个参数调用两种Type即可:

type Enum<T extends readonly string[], N extends boolean = false> = N extends true ? MergeObject<EnumToObjectNumber<T>> : EnumToObject<T>

const stringify = (data) => {
  const loop = (val) => {
    let res = '';
    if (typeof val === 'object' && val !== null) {
      // array
      if (Array.isArray(val)) {
        res += '[';

        res += `${val}`;

        return res + ']';
      } else {
        // object
        res += '{';
        const keys = Object.keys(val);
        keys.forEach((key, index) => {
          const itemVal = val[key];
          if (typeof itemVal === 'object' && itemVal !== null) {
            if (index === keys.length - 1) {
              res += `"${key}":${loop(itemVal)}`;
            } else {
              res += `"${key}":${loop(itemVal)},`;
            }
          } else {
            if (index === keys.length - 1) {
              res += `"${key}":${itemVal}`;
            } else {
              res += `"${key}":${itemVal},`;
            }
          }
        });

        return res + '}';
      }
    } else {
      // other
      return `${val}`;
    }
  };

  console.log(loop(data));
};

const mock = {
  a: 1,
  b: 2,
  c: {
    d: {
      e: 3,
    },
  },
  f: [4, 5],
  g: null,
  h: {
    i: {
      j: 6,
    },
  },
};

console.log(stringify(mock));
console.log(JSON.stringify(mock));