解决 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 A
、keyof B
代替 X
与 Y
,并交替 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 必定晓得传入类型是否为联结类型,并且会对联结类型进行非凡解决,但并没有裸露联结类型的判断语法,所以咱们只能对传入类型进行测试,推断是否为联结类型。
咱们到当初能想到联结类型的特色只有两个:
- 在 TS 解决泛型为联结类型时进行散发解决,行将联结类型拆解为独立项一一进行断定,最初再用
|
连贯。 - 用
[]
包裹联结类型能够躲避散发的个性。
所以怎么断定传入泛型是联结类型呢?如果泛型进行了散发,就能够反推出它是联结类型。
难点就转移到了:如何判断泛型被散发了?首先剖析一下,散发的成果是什么样:
A extends A
// 如果 A 是 1 | 2,散发后果是:(1 extends 1 | 2) | (2 extends 1 | 2)
也就是这个表达式会被执行两次,第一个 A
在两次值别离为 1
与 2
,而第二个 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 许可证)