TypeScript类型体操挑战(十一)

271 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

中等

ReplaceKeys

挑战要求

在线示例

type ReplaceKeys<U, T extends string, Y> = 
U extends U 
? {
    [P in keyof U]: P extends T ? (P extends keyof Y ? Y[P] : never) : U[P]
  } 
: U;
  • U将是一个联合类型,我们要对其中的每一个对象类型都进行处理,所以要让其发生分布行为,用U extends U就可以实现

  • 具体要处理的是对象中键对应的值类型,所以要遍历每个对象

/*
  P extends T:用于确认是否对要当前键进行处理
  (P extends keyof Y ? Y[P] : never):因为不能直接使用 Y[P],所以要进行类型推断
*/
{
    [P in keyof U]: P extends T ? (P extends keyof Y ? Y[P] : never) : U[P]
}

Remove Index Signature

挑战要求

在线示例

type RemoveIndexSignature<T> = {
  [P in keyof T as (P extends `${infer K}` ? K : never)]: T[P];
}
  • 键是可通过as进行重新映射的,所以这时可以使用模板字符串进行扩展或条件判断
  • 而通过${infer K}可以推断出键的字面量类型,所以能够排除掉索引类型,获取到普通的键

答案参考自解答区,大佬也有详细的解释,并对case进行了扩展。

Percentage Parser

挑战要求

在线示例

// 检查字符串前缀
type CheckPrefix<T extends string> = T extends '+' | '-' ? T : never;

// 检查字符串中的数值与 %
type CheckSuffix<T extends string> = T extends `${infer R}%` ? [R, '%'] : [T, ''];

type PercentageParser<A extends string> = A extends `${CheckPrefix<infer F>}${infer E}` ? [F, ...CheckSuffix<E>] : ['', ...CheckSuffix<A>];
  • 首先使用CheckPrefix<infer F>将字符串的第一个字符进行处理,从而判断出是不是指定的前缀
  • 之后就是根据不同的判断结果组装数组,CheckSuffix专门用来处理字符串中的数值与百分比符号

Drop Char

挑战要求

在线示例

type DropChar<S, C> = 
  S extends `${infer F}${infer E}` 
  ? `${F extends C ? '' :  F}${DropChar<E, C>}` 
  : S;
  • 遍历字符串S,然后对第一个字符F进行处理,字符串E则使用DropChar进行处理

MinusOne

挑战要求

在线示例

// 数值大小与元组长度相等的映射对象
type DigitToArray = {
  "0": [],
  "1": [unknown],
  "2": [unknown, unknown],
  "3": [unknown, unknown, unknown],
  "4": [unknown, unknown, unknown, unknown],
  "5": [unknown, unknown, unknown, unknown, unknown],
  "6": [unknown, unknown, unknown, unknown, unknown, unknown],
  "7": [unknown, unknown, unknown, unknown, unknown, unknown, unknown],
  "8": [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown],
  "9": [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]
};

// N 是一个转换为字符串的数值,根据它创建一个长度为 N 的元组
type CreateArrayByLength<N extends string, R extends unknown[] = []> = N extends `${infer First}${infer Rest}`
? First extends keyof DigitToArray
  ? CreateArrayByLength<Rest, [...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...DigitToArray[First]]>
  : never
: R;


// 主类型   
type MinusOne<T extends number> = CreateArrayByLength<`${T}`> extends [infer First, ...infer Rest]
? Rest["length"]
: never;

我们根据实际的case进行剖析理解:

1. Expect<Equal<MinusOne<1>, 0>>

// 1. T = 1,代入主类型得到:
type MinusOne = CreateArrayByLength<`${1}`> extends [infer First, ...infer Rest]
? Rest["length"]
: never;

接着看CreateArrayByLength类型:

// 1.1 代入类型中,此时 N = 1,R = [],所以得到:
type CreateArrayByLength = '1' extends `${infer First}${infer Rest}`
? First extends keyof DigitToArray
  ? CreateArrayByLength<Rest, [...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...DigitToArray[First]]>
  : never
: R;

// 1.2 '1' extends `${infer First}${infer Rest}` 与 First extends keyof DigitToArray 都是成立的,所以有:
CreateArrayByLength<'', [...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...DigitToArray['1']]>
// 相当于
CreateArrayByLength<'', [unknown]>;

// 1.3 此时 N = '',R = [unknown],所以得到:
type CreateArrayByLength = '' extends `${infer First}${infer Rest}`
? First extends keyof DigitToArray
  ? CreateArrayByLength<Rest, [...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...DigitToArray[First]]>
  : never
: R;
// 空字符串的判断条件不成立,最后直接返回 R,也就是 [unknown]
[unknown]         

根据 1.3 得到的结果,我们代入到主类型中:

type MinusOne = [unknown] extends [infer First, ...infer Rest]
? Rest["length"]
: never;
// 判断条件成立,则有:
type MinusOne = []["length"]

元组类型是允许推断出长度的,所以可以通过length属性获取到长度值,最后结果就是 0。

2. Expect<Equal<MinusOne<55>, 54>>

我这里就直接跳过第一步了,进入到 1.1 步骤:

// 1.1 代入类型中,此时 N = 55,R = [],所以得到:
type CreateArrayByLength = '55' extends `${infer First}${infer Rest}`
? First extends keyof DigitToArray
  ? CreateArrayByLength<Rest, [...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...DigitToArray[First]]>
  : never
: R;

// 1.2 两个判断条件都是成立的,所以有:
CreateArrayByLength<'5', [...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...DigitToArray['5']]>
// 相当于, 第二个参数的数组长度此时为 5
CreateArrayByLength<'5', [unknown, unknown, unknown, unknown, unknown]>;

// 1.3 根据1.2得到的结果,再次调用CreateArrayByLength
// 此时 N = '5',R = [ 这里面是 5 个unknown],并且两个条件判断都会成立,所以得到:
type CreateArrayByLength = '5' extends `${infer First}${infer Rest}`
? '5' extends keyof DigitToArray
  ? CreateArrayByLength<'', [...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...R, ...DigitToArray['5']]>
  : never
: R;
// 相当于
type CreateArrayByLength = CreateArrayByLength<'', [<这里有 50unknown>, <这里是 5unknown>]>
                        
// 1.4 空字符串的判断条件不成立,最后直接返回 R,也就是:
[<这里有 50unknown>, <这里是 5unknown>]    

根据 1.4 得到的结果,我们代入到主类型中:

type MinusOne = [<这里有 50unknown>, <这里是 5unknown>] extends [infer First, ...infer Rest]
? Rest["length"]
: never;
// 判断条件成立,则有:
type MinusOne = [<这里有 54unknown>]["length"]

最终结果就是 54。

总结:

  • 第一步就是先将数值转为字符串,然后遍历每一个字符串,得到长度等于该数值的一个元组
    • 每一次将调用CreateArrayByLength的时候,都会将当前的R的数量 * 10 再加上当前N值获取的元组数量,形成一个累加,再进行传递
  • 第二部就是通过条件判断 + rest参数,从而达到元组长度 - 1 的效果
  • 最后根据通过新元组就能获取到对应的长度了

参考自解答区,有些答案非常复杂,这个比较好理解。