关于前端:精读Diff-AnyOf-IsUnion

10次阅读

共计 5466 个字符,预计需要花费 14 分钟才能阅读完成。

解决 TS 问题的最好方法就是多练,这次解读 type-challenges Medium 难度 25~32 题。

精读

Diff

实现 Diff<A, B>,返回一个新对象,类型为两个对象类型的 Diff:

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

Equal<Diff<Foo, Bar> // {gender: number}

首先要思考 Diff 的计算形式,A 与 B 的 Diff 是找到 A 存在 B 不存在,与 B 存在 A 不存在的值,那么正好能够利用 Exclude<X, Y> 函数,它能够失去存在于 X 不存在于 Y 的值,咱们只有用 keyof Akeyof B 代替 XY,并交替 A、B 地位就能失去 Diff:

// 本题答案
type Diff<A, B> = {[K in Exclude<keyof A, keyof B> | Exclude<keyof B, keyof A>]:
    K extends keyof A ? A[K] : (K extends keyof B ? B[K]: never
    )
}

Value 局部的小技巧咱们之前也提到过,即须要用两套三元运算符保障拜访的下标在对象中存在,即 extends keyof 的语法技巧。

AnyOf

实现 AnyOf 函数,任意项为真则返回 true,否则返回 false,空数组返回 false

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

本题有几个问题要思考:

第一是用何种断定思路?像这种判断数组内任意元素是否满足某个条件的题目,都能够用递归的形式解决,具体是先判断数组第一项,如果满足则持续递归判断残余项,否则终止判断。这样能做但比拟麻烦,还有种取巧的方法是利用 extends Array<> 的形式,让 TS 主动帮你遍历。

第二个是如何判断任意项为真?为真的状况很多,咱们尝试枚举为假的 Case:0 undefined '' undefined null never []

联合下面两个思考,本题作如下解答不难想到:

type Falsy = '' | never | undefined | null | 0 | false | []
type AnyOf<T extends readonly any[]> = T extends Falsy[] ? false : true

但会遇到这个测试用例没通过:

AnyOf<[0, '', false, [], {}]>

如果此时把 {} 补在 Falsy 里,会发现除了这个 case 外,其余判断都挂了,起因是 {a: 1} extends {} 后果为真,因为 {} 并不示意空对象,而是示意所有对象类型,所以咱们要把它换成 Record<PropertyKey, never>,以锁定空对象:

// 本题答案
type Falsy = '' | never | undefined | null | 0 | false | [] | Record<PropertyKey, never>
type AnyOf<T extends readonly any[]> = T extends Falsy[] ? false : true

IsNever

实现 IsNever 判断值类型是否为 never

type A = IsNever<never>  // expected to be true
type B = IsNever<undefined> // expected to be false
type C = IsNever<null> // expected to be false
type D = IsNever<[]> // expected to be false
type E = IsNever<number> // expected to be false

首先咱们能够毫不犹豫的写下一个谬误答案:

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

这个谬误答案离正确答案必定是比拟近的,但错在无奈判断 never 上。在 Permutation 全排列题中咱们就意识到了 never 在泛型中的特殊性,它不会触发 extends 判断,而是间接终结,以致判断有效。

而解法也很简略,只有绕过 never 这个个性即可,包一个数组:

// 本题答案
type IsNever<T> = [T] extends [never] ? true :false

IsUnion

实现 IsUnion 判断是否为联结类型:

type case1 = IsUnion<string>  // false
type case2 = IsUnion<string|number>  // true
type case3 = IsUnion<[string|number]>  // false

这道题齐全是脑筋急转弯了,因为 TS 必定晓得传入类型是否为联结类型,并且会对联结类型进行非凡解决,但并没有裸露联结类型的判断语法,所以咱们只能对传入类型进行测试,推断是否为联结类型。

咱们到当初能想到联结类型的特色只有两个:

  1. 在 TS 解决泛型为联结类型时进行散发解决,行将联结类型拆解为独立项一一进行断定,最初再用 | 连贯。
  2. [] 包裹联结类型能够躲避散发的个性。

所以怎么断定传入泛型是联结类型呢?如果泛型进行了散发,就能够反推出它是联结类型。

难点就转移到了:如何判断泛型被散发了?首先剖析一下,散发的成果是什么样:

A extends A
// 如果 A 是 1 | 2,散发后果是:(1 extends 1 | 2) | (2 extends 1 | 2)

也就是这个表达式会被执行两次,第一个 A 在两次值别离为 12,而第二个 A 在两次执行中每次都是 1 | 2,但这两个表达式都是 true,无奈体现散发的特殊性。

此时要利用包裹 [] 不散发的个性,即在散发后,因为在每次执行过程中,第一个 A 都是联结类型的某一项,因而用 [] 包裹后必然与原始值不相等,所以咱们在 extends 散发过程中,再用 [] 包裹 extends 一次,如果此时匹配不上,阐明产生了散发:

type IsUnion<A> = A extends A ? ([A] extends [A] ? false : true
) : false

但这段代码仍然不正确,因为在第一个三元表达式括号内,A 曾经被散发,所以 [A] extends [A] 即使对联结类型也是断定为真的,此时须要用原始值代替 extends 前面的 [A],骚操作呈现了:

type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true
) : false

尽管咱们申明了 B = A,但过程中因为 A 被散发了,所以运行时 B 是不等于 A 的,才使得咱们达成目标。[B]extends 后面是因为,B 是未被散发的,不可能被散发后的后果蕴含,所以散发时此条件必然为假。

最初因为测试用例有一个 never 状况,咱们用方才的 IsNever 函数提前判否即可:

// 本题答案
type IsUnion<A, B = A> = IsNever<A> extends true ? false : (
  A extends A ? ([B] extends [A] ? false : true
  ) : false
)

从该题咱们能够粗浅领会到 TS 的怪异之处,即 type X<T> = T extends ...extends 后面的 T 不肯定是你看到传入的 T,如果是联结类型的话,会散发为单个类型别离解决。

ReplaceKeys

实现 ReplaceKeys<Obj, Keys, Targets>Obj 中每个对象的 Keys Key 类型转化为合乎 Targets 对象对应 Key 形容的类型,如果无奈匹配到 Targets 则类型置为 never

type NodeA = {
  type: 'A'
  name: string
  flag: number
}

type NodeB = {
  type: 'B'
  id: number
  flag: number
}

type NodeC = {
  type: 'C'
  name: string
  flag: number
}


type Nodes = NodeA | NodeB | NodeC

type ReplacedNodes = ReplaceKeys<Nodes, 'name' | 'flag', {name: number, flag: string}> // {type: 'A', name: number, flag: string} | {type: 'B', id: number, flag: string} | {type: 'C', name: number, flag: string} // would replace name from string to number, replace flag from number to string.

type ReplacedNotExistKeys = ReplaceKeys<Nodes, 'name', {aa: number}> // {type: 'A', name: never, flag: number} | NodeB | {type: 'C', name: never, flag: number} // would replace name to never

本题别看形容很吓人,其实非常简单,思路:用 K in keyof Obj 遍历原始对象所有 Key,如果这个 Key 在形容的 Keys 中,且又在 Targets 中存在,则返回类型 Targets[K] 否则返回 never,如果不在形容的 Keys 中则用在对象里原本的类型:

// 本题答案
type ReplaceKeys<Obj, Keys, Targets> = {[K in keyof Obj] : K extends Keys ? (K extends keyof Targets ? Targets[K] : never
  ) : Obj[K]
}

Remove Index Signature

实现 RemoveIndexSignature<T> 把对象 <T> 中 Index 下标移除:

type Foo = {[key: string]: any;
  foo(): void;}

type A = RemoveIndexSignature<Foo>  // expected {foo(): void }

该题思考的重点是如何将对象字符串 Key 辨认进去,能够用 \`${infer P}\` 是否能辨认到 P 来判断以后是否命中了字符串 Key:

// 本题答案
type RemoveIndexSignature<T> = {[K in keyof T as K extends `${infer P}` ? P : never]: T[K]
}

Percentage Parser

实现 PercentageParser<T>,解析出百分比字符串的符号位与数字:

type PString1 = ''type PString2 ='+85%'type PString3 ='-85%'type PString4 ='85%'type PString5 ='85'type R1 = PercentageParser<PString1> // expected ['', '','']
type R2 = PercentageParser<PString2> // expected ["+", "85", "%"]
type R3 = PercentageParser<PString3> // expected ["-", "85", "%"]
type R4 = PercentageParser<PString4> // expected ["","85","%"]
type R5 = PercentageParser<PString5> // expected ["","85",""]

这道题充分说明了 TS 没有正则能力,尽量还是不要做正则的事件 ^_^。

回到正题,如果非要用 TS 实现,咱们只能枚举各种场景:

// 本题答案
type PercentageParser<A extends string> = 
  // +/-xxx%
  A extends `${infer X extends '+' | '-'}${infer Y}%`? [X, Y, '%'] : (
    // +/-xxx
    A extends `${infer X extends '+' | '-'}${infer Y}` ? [X, Y, ''] : (
      // xxx%
      A extends `${infer X}%` ? ['', X,'%'] : (// xxx 包含 ['100', '%', ''] 这三种状况
        A extends `${infer X}` ? ['', X,'']: never
      )
    )
  )

这道题使用了 infer 能够有限进行分支判断的常识。

Drop Char

实现 DropChar 从字符串中移除指定字符:

type Butterfly = DropChar<'b u t t e r f l y !', ''> //'butterfly!'

这道题和 Replace 很像,只有用递归一直把 C 排除掉即可:

// 本题答案
type DropChar<S, C extends string> = S extends `${infer A}${C}${infer B}` ? 
  `${A}${DropChar<B, C>}` : S

总结

写到这,越发感觉 TS 尽管具备图灵齐备性,但在逻辑解决上还是不如 JS 不便,很多设计计算逻辑的题目的解法都不是很优雅。

然而解决这类题目有助于强化对 TS 根底能力组合的了解与综合使用,在解决理论类型问题时又是必不可少的。

探讨地址是:精读《Diff, AnyOf, IsUnion…》· Issue #429 · dt-fe/weekly

如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>

版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)

正文完
 0