深入typescript之‘可选的’和‘必要的’

Posted 恪愚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入typescript之‘可选的’和‘必要的’相关的知识,希望对你有一定的参考价值。

随着前端项目的规模不断变大,多人协同开发越来越受到所有公司的欢迎。随之而来的就是 TypeScript 被越来越多的项目所使用,这种变化并不是对技术的盲目追求,而是业务驱动下的技术进步,TypeScript 通过对原生 javascript 提供强类型加持,在很大程度上提升了代码质量,大大降低了多人协同场景下不同模块接口相互调用可能出现的隐性 bug。

比如:如果一个接口的某个属性是非必要的,那么你可以使用可选?:,但是

  1. 如果一个接口的属性在有的继承中是必要的,而在另一些时候是非必要的呢?

  2. 如果一个接口中的某一个/些属性是不能要的呢?

Partial:可选

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 在属性上有一种继承关系!

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

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
})

3 补充

之前曾经专门讲过 keyof 修饰符 —— 它返回一个联合类型。这里增加一个对 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"

Required :必要的

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 。

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'
}

Omit<Type, Keys>:忽略

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

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 的实现原理也很简单:在typescript中,never修饰的属性永远“不可到达”。但这里只需要知道功能即可。

有人疑惑于Exclude, T似乎比U的范围大一些 ? 也正是因为这样,所以T中的属性总有不是从U中extends过来的, 我们需要的也是这些属性 !

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

2 实战用法

interface Person {
    name: string
    age: number
    id: string
    work: string
    address: string
    girlFriend: number
}

// 没女朋友的人
type PersonNoGirlFriend =Omit<Person, 'girlFriend'>
//相当于:
type PersonNoGirlFriend =Pick<Person, 'name' | 'age' | 'id' | 'work' | 'address'>

源码和代码中我们可以看到一个新的类型 —— Pick。它的意思是“挑取”。

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

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

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

以上是关于深入typescript之‘可选的’和‘必要的’的主要内容,如果未能解决你的问题,请参考以下文章

Swift之深入解析如何处理非可选的可选项类型

发布 TypeScript 包时如何处理可选的对等依赖项?

typescript 可选的路由参数

typescript 可选的路由参数

typescript 可选的论证者

你可以在 Typescript 函数中有可选的解构参数吗?