/**
@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));