写点什么

玩转 TypeScript 工具类型(上)

发布于: 10 小时前
玩转TypeScript 工具类型(上)


联系我们有道技术团队助手:ydtech01 / 邮箱ydtech@rd.netease.com


随着前端项目的规模不断变大,多人协同开发已经成为了前端开发的标配,随之而来的就是 TypeScript 被越来越多的项目所使用,这种变化并不是对技术的盲目追求,而是业务驱动下的技术进步,TypeScript 通过对原生 JavaScript 提供强类型加持,在很大程度上提升了代码质量,大大降低了多人协同场景下不同模块接口相互调用可能出现的隐性 bug。本系列分享来源于我日常开发中对 TypeScript 工具类型的一些学习和使用体会,本系列文章分为上中下三篇,通过本系列分享,希望可以达到如下几个目的:


  • 从源码的角度理解每一种工具类型的实现机制

  • 通过一两个简单的例子了解每一种工具类型的基本用法

  • 与此同时加深对 TypeScript 的理解

  • 最终实现可以在实际工作中举一反三

一、 Partial<Type>:可选

1.1 源码解读

Partial<Type>类型的源码如下所示:


type Partial<T> = {    [P in keyof T]?: T[P];};
复制代码


这里需要关注四个点:


  • <T>:这是目标类型,也就是我们要做处理的类型,类型不确定,所以用泛型 T 表示

  • [P in keyof T] :keyof T 返回 T 类型的所有键组成的一个类型,in 可以按照 js 中的 for..in 遍历去理解,后续对 keyof 有更详细的说明

  • ?:可选,把返回类型的所有属性都转为可选类型

  • 返回的是一个新类型,这个新类型来源于 T,并且和 T 在属性上有一种继承关系,在第 2 小节对Required<Type>的说明中会验证这一点


基于对源码的理解,就可以很好的理解Partial<Type>类型的作用就是返回一个新类型,这个新类型和目标类型 T 拥有相同的属性,但所有属性都是可选的

1.2 实战用法

场景说明:在实际的业务开发中,经常会遇到需要对一个数据对象做整体或者局部更新的需求,这里就可以用到Partial<Type>


interface DataModel {  name: string  age: number  address: string}
let store: DataModel = { name: '', age: 0, address: ''}
function updateStore ( store: DataModel, payload: Partial<DataModel>):DataModel { return { ...store, ...payload }}
store = updateStore(store, { name: 'lpp', age: 18})
复制代码


亲手试一试

1.3 补充

这里增加一个对 keyof 的说明,通过一段代码来理解一下:


interface Person {  name: string;  age: number;  location: string;}
type K1 = keyof Person; // "name" | "age" | "location"type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...type K3 = keyof { [x: string]: Person }; // string
const person: Person = { name: '', age: 0, location: ''}
type k11 = keyof typeof person; // "name" | "age" | "location"
复制代码


参考一:官方文档说明


参考二:What does “keyof typeof” mean in TypeScript?

二、 Required <Type>:必要的

2.1 源码解读

type Required<T> = {    [P in keyof T]-?: T[P];};
复制代码


这个类型的作用就是把类型 T 中的所有属性都转为可选属性


这里源码中使用了一个 -? 来标注属性为必填的属性,那么这个 -? 是否是必须的呢?因为我们理解的可选属性是用 ? 明确标识的才是可选的,如果我们把-? 去掉,为什么就无法实现 Required 的效果了呢?我们先写一个 MyRequired<T>,如下所示:


type MyRequired<T> = {  [P in keyof T]: T[P];};
interface Props { a?: number; b?: string;}
const obj: MyRequired<Props> = { a: 5};
复制代码


亲手试一试


上面的代码是没有类型错误的,为什么呢?因为如果只是[P in keyof T] ,P 中的属性会保留它自身在 T 中的可选性。即之前如果是必填的,在新类型中还是必填的,如果是选填的同理。有点类似一种“继承关系”。所以使用 -? 来清除可选性实现 Required 。当然,+? 也是有效的,所以参考 Partial<T>,可知 +? 的 +`是可以省略的

2.2 实战用法

Required<T>会将传入的 T 类型的所有属性都转为必要的。所以最常见的用法就是做诸如此类的转换,但是如果只是想把 T 类型的某些属性转为必填并把这些属性返回成一个新类型


interface Props {  a?: string  b?: string  c?: string}
// 仅保留b,c属性并转为必填type NewProps1 = Required<Pick<Props, 'b' | 'c'>>
// 需要保留Props的所有属性,但是b,c需要必填type NewProps2 = Partial<Props> & Required<Pick<Props, 'b' | 'c'>>
const obj: NewProps2 = { b: '1', c: '2'}
复制代码


亲手试一试

三、 ReadOnly<Type>:只读

3.1 源码解读

type Readonly<T> = {    readonly [P in keyof T]: T[P];};
复制代码


将类型 T 中包含的属性设置为 readonly,并返回一个新类型。readonly,顾名思义表示只读,初始化后就不能再修改值。这个类型可以配合 javascript 的 const 关键字实现引用类型属性值为常量的目的。


这个类型有一个局限性,就是只能设置子属性为只读,如果子属性还是一个引用类型,那对孙属性是不起作用的,那么有没有什么办法可以实现递归把所有引用都设置成只读的呢?


欢迎大家留言讨论。

3.2 实战用法

interface Person {    name: string    age: number}
const person: Readonly<Person> = { name: 'lpp', age: 18}
person.age = 20; // 无法分配到 "age" ,因为它是只读属性。ts(2540)
复制代码


如果没有 readonly ,在 javascript 中,如果给 const 变量赋值唯一个引用类型,比如一个对象,是可以修改属性值的,不能修改的是变量中存储的引用,如果要实现对象属性值的不可变,在 javascript 中可以使用 Object.freeze 。


function freeze<Type>(obj: Type): Readonly<Type>;
复制代码

四、 Record<Keys, Type>:记录

该工具类型会构造一个类型,这个类型的键的类型是 Keys,值的类型 Type 。

4.1 源码解读

type Record<K extends keyof any, T> = {    [P in K]: T;};
复制代码


这里我们看到对 K 的类型定义使用的是 keyof any。这里 keyof any string | number | symbol,如下所示:


type a = keyof any;// 等价于type a = string | number | symbol;
复制代码

4.2 实战用法

// 简单的限定键和值的类型type Obj1 = Record<string, string>
// 基于其他类型生成新的类型type FruitTypes = 'apple' | 'banana' | 'pear'
interface FruitInfo { name: FruitTypes price: number}
type Fruits = Partial<Record<FruitTypes, FruitInfo>>
const fruits: Fruits = { apple: { name: 'apple', price: 10 }}
复制代码


亲手试一试

五、Pick<Type, Keys>:挑取

从类型 Type 中,挑选一组属性组成一个新的类型返回。这组属性由 Keys 限定, Keys 是字符串或者字符串并集。

5.1 源码解读

type Pick<T, K extends keyof T> = {    [P in K]: T[P];};
复制代码


K extends keyof ,表示 K 需要是 keyof T 的子集。返回的类型的键需要满足[P in K],值类型满足 T[P]。

5.2 实战用法

interface Person {  name: string  age: number  id: string}
// 幼儿没有idtype Toddler = Pick<Person, 'name' | 'age'>
复制代码

六、 Omit<Type, Keys>:忽略

构造一个类型,这个类型包含类型 Type 中除了 Keys 之外的其余属性。 Keys 是一个字符串或者字符串并集。

6.1 源码解读

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;type Exclude<T, U> = T extends U ? never : T;
复制代码


因为 Omit 依赖了 Exclude ,所以这里把 Exclude 的类型源码一起贴在这里。Exclude<T, U>的作用是从 T 中排除那些可以分配给 U 的类型。


这里先不讲 Exclude 是如何实现的,只需要知道功能即可。


所以可以把 Exclude<keyof T, K> 看作是一个反选,选出了 T 中那些不包含在 K 中的属性,然后在用 Pick ,就实现了 Omit 。

6.2 实战用法

interface Person {  name: string  age: number  id: string  work: string  address: string  girlFriend: number}
// 没工作的人type PersonNoWork = Omit<Person, 'work'>
// 没住址的人type PersonNoAddress = Omit<Person, 'address'>
// 没女朋友的人type PersonNoGirlFriend =Omit<Person, 'girlFriend'
复制代码

七、练习题

如何实现一个工具类型 SelectRequired<T, K in keyof T> ,实现如下效果:


interface Props {  a?: string  b?: string  c?: string  d: string}
type NewProps = SelectRequired<Props, 'b' | 'c'>; // { a?: string, b: string, c: string, d: string }
复制代码


答案点这里

八、下回预告

在下一篇《玩转 TypeScript 工具类型(中)》里,将会包括如下内容,敬请期待:


  • 必读:extends 条件运算符

  • Exclude<Type, ExcludeUnion>

  • Extract<Type, Union>

  • NonNullable

  • 必读:tuple type 元组类型

  • Parameters

  • ConstructorParameters

  • ReturnType

  • InstanceType

发布于: 10 小时前阅读数: 3
用户头像

高效学习,从有道开始 2021.03.10 加入

分享有道人的技术思考与实践。

评论

发布
暂无评论
玩转TypeScript 工具类型(上)