extends用法梳理
做了type-challenges之后,记录对extends的理解。
# extends做条件批判联合类型
先看例子
type Test<T, T2 = T> = T extends T2 ? {t: T, t2: T2} : never
type u = Test<"2" | "3", "2" | "3">;
// 结果
type u = {
t: "2";
t2: "2" | "3";
} | {
t: "3";
t2: "2" | "3";
}
2
3
4
5
6
7
8
9
10
如上:
当extends做条件限制联合类型 T extends T2 时,所作的比对是 "2" | "3" extends "2" | "3",它在ts中其实会被转换成 ("2" extends "2" | "3") | ("3" extends "2" | "3"), 即将联合类型中的T的内容遍历出来与T2进行比对。随后将结果以联合类型返回。
那么当我们想要去判断联合类型入参T是否与T2全等时, 我们该怎么做呢?
type Test<T, T2 = T> = [T] extends [T2] ? {t: T, t2: T2} : never
type u = Test<"2" | "3", "2" | "3">;
// 结果
type u = {
t: "2" | "3";
t2: "2" | "3";
}
2
3
4
5
6
7
我们可以发现,放置方括号以避免这次分配,即转变成比对数组内元素的类型是否相等。
我们来通过拆解道题加深理解IsUnion (opens new window):
type case1 = IsUnion<string>; // false
type case2 = IsUnion<string | number>; // true
type case3 = IsUnion<[string | number]>; // false
type IsUnion<T, T2 = T> = T extends T2
? [T2] extends [T]
? false
: true
: never;
// 当 T extends T2 即我们可以得到前面的一段结果
type Test<T, T2 = T> = [T] extends [T2] ? {t: T, t2: T2} : never
type u = Test<"2" | "3">;
// 结果
type u = {
t: "2";
t2: "2" | "3";
} | {
t: "3";
t2: "2" | "3";
}
// 此时经过T extends T2 ? 加工后 的T 值已经变成了联合类型中的单个变量"2"和"3", 接下来 [T2] extends [T] 即
["2" | "3"] extends ["2"] ?
["2" | "3"] extends ["3"] ?
// 通过增加方括号消除了extends对联合类型分配的约束。很显然此时的"2" | "3"是不属于["2"] 的,返回false。那么当我们输入单个字符时,即
type u = Test<"2">;
"2" extends "2"? // true
["2"] extends ["3"] ? // true
// 显然就不是元组类型了。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 两个接口类型的比较
X extends Y − 判断 X 是否为 Y 的子类型
type ButtonProps = {
size: 'mini' | 'large',
type: 'primary' | 'default',
}
type MyButtonProps = {
size: 'mini',
type: 'primary' | 'default',
color: 'red' | 'blue'
}
type IsSubButton = MyButtonProps extends ButtonProps ? true : false; // true
2
3
4
5
6
7
8
9
10
11
12
对于两个接口类型,extends 会先将接口类型中的每一个属性拿出来比较, 相当于
type ButtonProps = 'size' | 'type'
type MyButtonProps = 'size' | 'type' | 'color'
MyButtonProps extends ButtonProps
// 等同于
'size' extends 'size' | 'type' | 'color' ? true : false
'type' extends 'size' | 'type' | 'color' ? true : false
2
3
4
5
6
7
- 当extends发现 两者之中后者的属性比对结果都为 true 后,再拿出属性的内容进行比对(可以理解为取包含关系):
// size
'mini' extends 'mini' | 'large' ? true : false
'primary' | 'default' extends 'primary' | 'default' ? true : false
// =>等同于
'primary'extends 'primary' | 'default' &'default'extends 'primary' | 'default'? true : false
2
3
4
5
- 可以看到 'mini' 属于联合类型 'mini' | 'large' 中的一份子,即可由其生成,返回true,同理 'primary' | 'default' 也是如此,由此得出 MyButtonProps extends ButtonProps 的结果为true
我们再看增加了一个属性之后的结果
type ButtonProps = {
...
type2: 'primary' | 'default',
}
type IsSubButton = MyButtonProps extends ButtonProps ? true : false; // false
2
3
4
5
由上面的第一步可以得知, MyButtonProps的属性里并没有 type2 这个类型,所以不能由 ButtonProps 获得,返回 false
对于属性的值的判断,同时还包括类型缩减
type ButtonProps = {
// size: 'mini' | 'large',
// type: 'primary' | 'default',
size: string
}
type MyButtonProps = {
size: 'mini',
type: 'primary' | 'default',
color: 'red' | 'blue'
}
type IsSubButton = MyButtonProps extends ButtonProps ? true : false; // true
type ButtonProps = {
size: 'mini' | 'large',
}
type MyButtonProps = {
size: string,
type: 'primary' | 'default',
color: 'red' | 'blue'
}
type IsSubButton = MyButtonProps extends ButtonProps ? true : false; // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
我们将size值更换为了string类型,string类型和MyButtonProps的size(字面量string类型)来说,他们是父子的关系,所以MyButtonProps的size可以由 ButtonProps 的size继承得出,返回true.
反过来MyButtonProps值为了string类型时,则是出现了类型拓宽,很显然ButtonProps的size子类型并不能变成string父类型,返回false
# extends判断字符串
当我们需要书写对字符串进行操作的TS时,我们常常通过 extends
${xxx}
或是 extends${infer X}
来展开实现。
- 当我们使用
${xxx}
时:
type IsSame<T extends string, K extends string> = T extends `${K}` ? true:false;
type w = IsSame<'abc', 'abc'> // true
type w = IsSame<'abc', 'abd'> // false
2
3
显然得知:当我们在字符串之间进行比较时, extends会同等的比较一整个字符串是否全等。
- 当我们采用
${infer X}
时:
type GetAll<T extends string> = T extends `${infer K}`
? {
v:K
}
: T;
type q = GetAll<'abc'> // { v: "abc"; }
type GetFirst<T extends string> = T extends `${infer K}${infer U}`
? {
v:K
}
: T;
type w = GetFirst<'abc'> // { v: "a";}
type d = GetFirst<'bc'>// { v: "b";}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
从上我们可以看出,当我们采用${infer K}
(infer 我们可以理解成将ts类型参数化)时,我们返回的是一整个string字符串, 当我们${infer K}${infer U}
时,每一次返回的都是字符串的首字母,我们通常借由这点通过递归的形式遍历整个字符串,举个例子:
对于${infer K}
:
// 实现 Capitalize<T> 它将字符串的第一个字母转换为大写,其余字母保持原样。
type Capitalize<T extends string> = T extends `${infer U}${infer K}`
? `${Uppercase<U>}${K}`
: T;
type capitalize = Capitalize<"hello world">; // expected to be 'Hello world'
2
3
4
5
这里我们通过infer拿到首字母,并通过字符串方法Uppercase将首字母转换成大写抛出。
// 实现一个将接收到的String参数转换为一个字母Union的类型。
type StringToUnion<T extends string, K = never> = T extends `${infer V}${infer U}`? V extends never ? K : StringToUnion<U, K | V> : K;
type Test = '123';
type Result = StringToUnion<Test>; // expected to be "1" | "2" | "3"
2
3
4
5
6
这里我们通过infer拿到首字母, 再进入StringToUnion遍历字符串的所有字母从而达到了将字符串转换为联合类型的目的。 对于字符串的操作同样适用于元组