0%

typescript-pick-optional-types

什么是可选属性?

今天我们来看一下如何从一个 TypeScript 类型中提取可选属性。那么什么是可选属性呢?
可选属性是指在类型定义中使用问号(?)标记的属性,这些属性在对象中可以存在也可以不存在。

以下面的User类型为例:其中id和name是必需的属性,而age和email是可选的属性。

1
2
3
4
5
6
interface User {
id: number;
name: string;
age?: number; // 可选属性
email?: string; // 可选属性
}

对于必须属性,我们在定义变量的时候必须提供这些属性的值。

下面这个变量定义是正确的,因为id和name这两个必须属性都出现了。

1
2
3
4
const user1: User = {
id: 1,
name: 'Alice',
}

下面这个变量定义则是错误的,因为缺少了id属性。

1
2
3
4
const user2: User = {
name: 'Philip',
age: 18
};

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属性的类型是numberemail属性的类型是string

如果可选属性未出现,那么它的类型就是undefined,比如上面的user1变量中,user1.ageuser1.email的类型都是undefined

综合下来,我们可以得出一个结论:可选属性的类型是T | undefined,其中T是可选属性的定义类型。

User类型复制到IDE中,可以将鼠标悬停在ageemail属性上,查看它们的类型。

1
2
age 对应的类型是 number | undefined
email 对应的类型是 string | undefined

提取可选属性

可选属性介绍完毕,,现在问题来了,如何从一个类型定义中提取出所有可选属性,对应上面的User类型,我们需要提取出ageemail这两个属性。

我们可以分步骤解决这个问题,每个步骤解决一个问题

第一步:获取所有属性

要提取可选属性,我们首先需要获取类型中所有的属性。TypeScript提供了内置的keyof操作符,可以获取一个类型的所有键(属性)。

假设给定的是一个类型T,那么keyof T将返回一个联合类型,包含T的所有属性名。

1
type UserKeys = keyof User; // "id" | "name" | "age" | "email"

假设给定的不是一个类型,而是一个变量,那么首先要用typeof操作符获取变量对应的的类型。再使用keyof获取该类型所有属性。

1
2
3
4
5
6
7
const user = {
id: 1,
name: 'Alice',
age: 30,
};

type UserKeys = keyof typeof user; // "id" | "name" | "age"

可以看到虽然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,接下来我们只需要把这两步结合起来即可:

  1. 对于一个给定的类型T,我们首先获取其所有属性。
  2. 遍历步骤1中获取的所有属性,对于每个属性K,使用IsOptional<T, K>判断它是否是可选的。
  3. 如果是可选的,就将其包含在结果中,否则将其排除。

代码如下:

1
2
3
type OptionalKeys<T> = keyof {
[K in keyof T as IsOptional<T, K> extends true ? K : never]: any;
};

这段代码的核心部分在于[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是我们提取的可选属性,对象的值就是anyany只是用来来占位的,即使使用其他类型也是一样的,比如unknown或者void都可以。

循环遍历完成后,我们得到一个对象如下:

1
2
3
4
{
age?: any;
email?: any;
}

最后再使用使用keyof操作符获取这个对象的所有键,就得到了可选属性的联合类型:"age" | "email"

1
2
3
4
keyof {
age?: any;
email?: any;
} // "age" | "email"

因此,最终的OptionalKeys<User>将返回"age" | "email"

好了,今天就到这里了,感谢大家的支持!我们明天见。

其实这篇文章是分两次写的,昨天晚上写了一半困得不行,遂作罢,今天早上又起来继续写。日更不能停啊。