学习Typescript(5)高级语法3 | 青训营笔记

73 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天

类型映射&条件类型

映射类型

TypeScript 提供了从旧类型中创建新类型的一种方式 — 映射类型(一般用到 in 遍历枚举类型的地方就是映射类型出现的地方)。

例如,定义一个接口 obj:

interface obj {

a : number;

b : string;

c : boolean;

}

现在要求把 obj 的元素变为只读,简单我们只需要:

interface obj {

readonly a : number;

readonly b : string;

readonly c : boolean;

}

这个有点麻烦 TS 为我们提供了映射类型可以方便这个操作,只需要一行代码:

//可以这样使用

var avc:Readonly;

//推荐这样使用

type ReadonlyObj = Readonly;

索引类型查询操作符 keyof T 我们知道是干啥的了,现在主要是弄懂 P 代表啥,也就是 in 操作符的使用,来一段代码瞬间看懂:

type Keys = 'name' | 'age';

type Flags = { [K in Keys]: boolean };

const flagObj:Flags= {

name:true,

age:false,

}

它的语法有三个部分:

类型变量K,它会依次绑定到联合类型的每个属性。字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。属性的结果类型。

等于号后面的看懂了,在看等于号前面的 type 定义类型别名,为了更方便的使用引入接口泛型。

映射类型常用的有以下几个:

必选属性变可选属性,案例如下:

interface obj {

a : number;

b : string;

c : boolean;

}

type ParticalObj = Partial;

/* type ParticalObj = {

a?: number;

b?: string;

c?: boolean;

}

*/

这个源代码是实现就更简单了,只需要加一个?。

抽取不部分必选属性:

interface obj {

a : number;

b : string;

c : boolean;

}

type pickObj = Pick;

/* type pickObj = {

a: number;

b: string;

} */

Pick 的实现源码:

/**

* From T, pick a set of properties whose keys are in the union K

*/

type Pick = {

[P in K]: T[P];

};
上面这三个都属于同态映射类型(homomorphic mapped types),意思是不会对原类型接口的结构进行改变,对应的非同态就是改变了原结构。再来介绍一个非同态:
interface obj {

a : number;

b : string;

c : boolean;

}

type recordObj = Record;

/* type recordObj = {

x: obj;

y: obj;

} */
得到的类型别名 recordObj 和 obj 的结构已经完全不一样的,这个就叫非同太映射类型。
我们已经知道了 同态类型(homomorphic types) 和 非同态类型(Non-homomorphic types)的区别就是结构不同。但是同态类型(homomorphic types)还有些特性,我们来仔细看看:
interface People {

readonly name?: string;

readonly age?: number;

};

type inherit = {

[ P in keyof T ]: T[P];

};

type legend = inherit;

/*

legend

type legend = {

readonly name?: string | undefined;

readonly age?: number | undefined;

}

*/

看到没,同态类型会完整的继承原类型结构上面的 readonly 和 ?:,这是它的一大特点。但是如果我们想打破同态这个特性怎么办呢?

第一个想到的就是,工具类型中的 Required Utility Types requiredtype。它就能把可选变为必须,我们来看看它的源代码:

type Required = { [P in keyof T]-?: T[P]; }

相信你也看到了,问好前面多了一个减号,猜你也猜出来它的作用了。它的教程在这:

用法很简单,通过加减号来控制是否添加删除 readonly 和 ?:

-readonly // Remove readonly

-? // Remove ?

+readonly // Add readonly

+? // Add ?

看例子一个添加和删除 readonly 和 ? 的例子:

// add readonly and ?

interface addPeople {

name: string;

age: number;

};

type add = {

+readonly [ P in keyof T ]+?: T[P];

};

type cloneAddPeople = add;

// type cloneAddPeople = {

// readonly name?: string | undefined;

// readonly age?: number | undefined;

// }

// remove readonly and ?

interface removePeople {

readonly name?: string;

readonly age?: number;

};

type remove = {

-readonly [ P in keyof T ]-?: T[P];

};

type cloneRemovePeople = remove;

// type cloneRemovePeople = {

// name: string;

// age: number;

// }

最后,来看下使用 Record 的结果:

type recordObj = Record;

/* type recordObj = {

x: number;

y: number;

} */

没错,这个完全可以改成 interface 接口来实现。

条件类型

TypeScript 2.8 引入了有条件类型,它能够表示非统一的类型.。

官网多个有字,无所谓了,就叫条件类型就行了,条件类型无疑是增加了类型的灵活性,这就是它最大的有点。一个标准的条件类型表达式为:

T extends U ? X : Y

上面的类型意思是,若 T 能够赋值给 U,那么类型是 X,否则为 Y。

官网示例:

type TypeName =

T extends string ? "string" :

T extends number ? "number" :

T extends boolean ? "boolean" :

T extends undefined ? "undefined" :

T extends Function ? "function" :

"object";

type T0 = TypeName; // "string"

type T1 = TypeName; // "string"

type T2 = TypeName; // "boolean"

type T3 = TypeName void>; // "function"

type T4 = TypeName; // "object"

看懂上面再来看分布式有条件类型:条件类型里面有联合类型

// (A | B) extends T ? X : Y;可分解为

// (A extends T ? X : Y) | (B extends T ? X : Y);

type TypeName =

T extends string ? "string" :

T extends number ? "number" :

T extends boolean ? "boolean" :

T extends undefined ? "undefined" :

T extends Function ? "function" :

"object";

// (A | B) extends T ? X : Y;可分解为

// (A extends T ? X : Y) | (B extends T ? X : Y);

type T5 = TypeName;

// type T5 = "string" | "number"

我们现在利用分布式条件类型,做个案例,从类型 T 中去除可以赋值给 U 的类型:

type Diff< T , U > = T extends U ? never : T;

type diff = Diff< 'a' | 'b' | 'c' |'d' , 'a' | 'b'>

猜猜上面代码的结果:

type diff = 'c' | 'd';

没猜对的话我给你分解下真正执行的代码:

('a' extends 'a' | 'b' ? never : 'a') | ('b' extends 'a' | 'b' ? never : 'b') | ('c' extends 'a' | 'b' ? never : 'c') | ('d' extends 'a' | 'b' ? never : 'd')

刚刚上面这个案例,从 T 中剔除可以赋值给 U 的类型,可以直接调用 TS 提供的 API。

type diffoo = Exclude< 'a' | 'b' | 'c' |'d' , 'a' | 'b'>

//Exclude 逻辑实现就是我们上面写的

Exclude 就是传说中的预定义的有条件类型。常见的有,我都不讲常用的:

TypeScript 2.8 在 lib.d.ts 里增加了一些预定义的有条件类型:

Exclude -- 从T中剔除可以赋值给U的类型。

Extract -- 提取T中可以赋值给U的类型。

NonNullable -- 从T中剔除null和undefined。

ReturnType -- 获取函数返回值类型。type fun = ReturnTypestring>

InstanceType -- 获取构造函数类型的实例类型。

这里只讲下 ReturnType 的实现,首先:

现在在有条件类型的 extends 子语句中,允许出现 infer 声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的 true 分支中被引用。 允许出现多个同类型变量的 infer。

提取函数类型的返回值类型就用到了 infer:

type ReturnType = T extends (...args: any[]) => infer R ? R : any;

用到 infer 就叫有条件类型中的类型推断,infer U 就表示不指定 U 的类型,你去类型推断 U 是啥类型,你也可以认为 U 就是 any 类型。