什么是可选属性?
今天我们来看一下如何从一个 TypeScript 类型中提取可选属性。那么什么是可选属性呢?
可选属性是指在类型定义中使用问号(?
)标记的属性,这些属性在对象中可以存在也可以不存在。
以下面的User
类型为例:其中id和name是必需的属性,而age和email是可选的属性。
1 | interface User { |
对于必须属性,我们在定义变量的时候必须提供这些属性的值。
下面这个变量定义是正确的,因为id和name这两个必须属性都出现了。
1 | const user1: User = { |
下面这个变量定义则是错误的,因为缺少了id属性。
1 | const user2: User = { |
TypeScript对于上面这个类型定义会给出如下错误:
1 | TS2741: Property 'id' is missing in type '{ name: string; }' but required in type 'User'. |
对于可选属性,当它未出现在变量中时,它的值就是undefined
(对应的类型也是undefined
),这个特性非常重要,后面我们会用到。比如对于上面的user1
来说,user
.age和user.email
的值都是undefined
。
可选属性的类型
可选属性出现时,那么它的类型就是定义的类型,比如上面的age
属性的类型是number
,email
属性的类型是string
。
如果可选属性未出现,那么它的类型就是undefined
,比如上面的user1
变量中,user1.age
和user1.email
的类型都是undefined
。
综合下来,我们可以得出一个结论:可选属性的类型是T | undefined
,其中T
是可选属性的定义类型。
将User
类型复制到IDE中,可以将鼠标悬停在age
和email
属性上,查看它们的类型。
1 | age 对应的类型是 number | undefined |
提取可选属性
可选属性介绍完毕,,现在问题来了,如何从一个类型定义中提取出所有可选属性,对应上面的User
类型,我们需要提取出age
和email
这两个属性。
我们可以分步骤解决这个问题,每个步骤解决一个问题
第一步:获取所有属性
要提取可选属性,我们首先需要获取类型中所有的属性。TypeScript提供了内置的keyof
操作符,可以获取一个类型的所有键(属性)。
假设给定的是一个类型T,那么keyof T
将返回一个联合类型,包含T的所有属性名。
1 | type UserKeys = keyof User; // "id" | "name" | "age" | "email" |
假设给定的不是一个类型,而是一个变量,那么首先要用typeof
操作符获取变量对应的的类型。再使用keyof
获取该类型所有属性。
1 | const user = { |
可以看到虽然user
变量属于User
类型,但是两者返回的属性并不完全相同,因为user
变量并未包含email
属性。
第二步:判断一个类型是否是可选的
typescript中并没有提供内置的操作符判断一个属性是否是可选的,但是我们可以通过条件类型来实现。
前面说过当可选类型未出现
时,他的值就是undefined
(类型也是undefined
),所以我们可以通过判断一个属性的类型是否包含undefined
来判断它是否是可选的。
下面我们来定义一个类型IsOptional
,它接受两个参数:一个类型T和一个属性K。它将返回一个布尔值,表示属性K是否是类型T的可选属性。
代码大概是这个样子的
1 | type IsOptional<T, K> = undefined extends T[K] ? true : false; |
如果K是可选的,那么T[K]的值就有可能是undefined
,因此undefined extends T[K]
将返回true
,否则返回false
。
举个例子:我们将T和K对应到文章开头的类型定义中,令T = User
, 假设K = age
,那么就有:
- 当
age
属性存在时:T[K] = number
- 当
age
属性不存在时:T[K] = undefined
所以T[K] = number | undefined
,因此undefined extends T[K]
将返回true
。
假设K = name
,因为name
属性是必需的,所以T[K] = string
,因此undefined extends T[K]
将返回false
。
似乎问题就要解决了,但是这里还有一个严重的问题,那就是K必须是T的属性,否则T[K]
将会报错。所以我们还要限制一下K的值。
1 | type IsOptional<T, K extends keyof T> = undefined extends T[K] ? true : false; |
K extends keyof T
的意思是K必须是T的属性之一,这样可以避免T[K]
报错。
第三步:提取可选属性
现在我们已经有了获取所有属性的类型UserKeys
和判断一个属性是否是可选的类型IsOptional
,接下来我们只需要把这两步结合起来即可:
- 对于一个给定的类型T,我们首先获取其所有属性。
- 遍历步骤1中获取的所有属性,对于每个属性K,使用
IsOptional<T, K>
判断它是否是可选的。 - 如果是可选的,就将其包含在结果中,否则将其排除。
代码如下:
1 | type OptionalKeys<T> = keyof { |
这段代码的核心部分在于[K in keyof T as IsOptional<T, K> extends true ? K : never]
- 这一行代码对应了我们上面的三个步骤。
keyof T
获取所有属性。K in keyof T
遍历所有属性。as IsOptional<T, K> extends true ? K : never
对于类型K,使用条件类型来判断属性K是否是可选的,如果是则保留K,否则将其排除(变为never
)。
但是最后为什么还有一个any呢?因为这是一个遍历操作,我们要把结果放到一个对象中,对象的key是我们提取的可选属性,对象的值就是any
,any
只是用来来占位的,即使使用其他类型也是一样的,比如unknown
或者void
都可以。
循环遍历完成后,我们得到一个对象如下:
1 | { |
最后再使用使用keyof
操作符获取这个对象的所有键,就得到了可选属性的联合类型:"age" | "email"
1 | keyof { |
因此,最终的OptionalKeys<User>
将返回"age" | "email"
。
好了,今天就到这里了,感谢大家的支持!我们明天见。
其实这篇文章是分两次写的,昨天晚上写了一半困得不行,遂作罢,今天早上又起来继续写。日更不能停啊。