TypeScript interface提取某个类型的属性名称
问题
今天遇到一个TypeScript需求,以下是个interface:
interface A {
a: string;
b: number;
c: boolean;
d: number;
}
但我想取到类型全是number的属性名称,也就是相当于:
type X = 'b' | 'd'
答案
在官方的Pick、Extract一堆内置的方法里找了半天,没有适合的。于是查了半天资料,最终写出来是这样的:
type PickTypes<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
使用:
type B = PickTypes<A, number>;
const a: B = "a"; // 报错
console.log(a);
const b: B = 'b'; // ok
const arr: B[] = ["a", "b"]; // 报错
console.log(arr);
在编辑里光标移动后B,会看到类型被正确推断出来
解释
type PickTypes<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
将这段代码拆分成两部分来理解:
type Temp<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}
type PickTypes<T, U> = type Temp<T, U>[keyof T]
第一部分就是对传递的参数T进行遍历,如果值是继承自U,则将该值设置成属性K,否则是never。
仍以上面的接口A为例,推导后的类型为:
第二部分是在上一部分的结果,再取下属性值。如果不好理解,可以先看这个推断:
type ValueTypes = A[keyof A];
相当于
type ValueTypes = A['a'] | A['b'] | A['c'] | A['d']
= string | number | boolean | number
= string | number | boolean
而对于我们这个场景,就是
type PickTypes<T, U> = test['a'] | test['b'] | test['c'] | test['d']
= never | 'b' | never | 'd'
= 'b' | 'd'
扩展
假设我们的接口有个为undefined的类型,那就推断不出来了。例如:
interface A {
a: string;
b: number;
c: boolean;
d?: number;
}
那么怎么办呢?
两种方式,一种是在调用PickTypes前,先将参数Required,比如:
PickTypes<Required<A>, number>
这种肯定不够优雅,如果PickTypes就能做到就最好了,所以第二种方式最终代码如下:
type PickTypes<T, U> = NonNullable<
{
[K in keyof T]: NonNullable<T[K]> extends U ? K : never;
}[keyof T]
>;
这里利用了NonNullable来去除undefined,其源码是这样的:
type NonNullable<T> = T extends null | undefined ? never : T
总结
TypesSript的类型系统设计的非常复杂,虽然高级的功能我们多数场景使用不到,但还是有必要深入理解学习的,尤其做底层框架或库开发时可以让你的代码更加健壮。
转载自:https://juejin.cn/post/7281491804737404982