构造类型抽象、TypeScript 编程内参(二)

本文是《TypeScript 编程内参》系列第二篇:构造类型抽象,主要记述 TypeScript 的高级使用方法和构造类型抽象。编程

PS: 本文语境下的「约束」指的是「类型对值的约束」数组

1、构造类型抽象

在 TS 的世界里,总有「动态地生成类型」的需求,好比下面的 UserWithHisBlogsUser 重复的部分:编程语言

type User = {
    id: number;
    name: string;
}

type UserWithHisBlogs = {
    id: number;
    name: string;
    blogs: Blog[]
}

type Blog = {
    id: number;
    content: string;
    title: string;
}
复制代码

上面的类型定义是存在冗余的,当 User 上面写了新的字段,咱们就不得不手工的去改 UserWithHisBlogs 以使其拥有刚刚新增的字段。函数

那么,有没有什么抽象的方法避免这个问题呢?有的, 利用 &:post

type UserWithHisBlogs = User & {
    blogs: Blog[];
}
复制代码

这里后文会解释 & 的含义,这里的 UserWithHisBlogs 跟一开始的例子里的类型彻底等价,惟一的区别是利用 & 关联了 User,避免了上面那个重复修改的问题。ui


这里只是个简单的引子,抽象的意义在于减小重复的事情,类型抽象的意义在于减小冗余的类型说明(减小重复的类型说明)spa

在实际 TS 编程的时候应该特别注意:经过构造类型抽象,尽可能复用原有的类型声明,避免重复声明。prototype

2、构造数/元组类型

咱们能够这样声明数组的类型:3d

type Arr = Array<any>;
// 这样也能够,跟上面几乎是等价的
type Arr = any[];
复制代码

可是这样声明的数组元素类型都是同样的,不少状况下咱们代码里面的数组里不一样位置的元素的类型是不同的,所以有了元组这样的类型抽象去约束数组元素:code

type NumStr = [number, string];
// 第三个元素不在 NumStr 里,会报错
const pair: NumStr = [1, '1', 'xxx'];

// 能够嵌套声明
type N = [[number, string], [number, string]]
const n: N = [[1, '1'], [2, '2']];
复制代码

3、构造联合/交叉类型

ts 的类型是能够计算的,经过不一样的运算符链接不一样的类型能够得出不一样的类型。


联合类型 Uinion Type 一般由 | 运算符链接两个类型得出来的,如 A | B 的意思是要么知足 A 的约束,要么知足 B 的约束 (知足一个便可)

能够参考下面的例子:

type Suffix = '先生' | '女士';
const sayName = (name: string, suffix: Suffix) => {
    return name + ' ' + suffix;
}
sayName('e', '先生');
sayName('c', '女士');
sayName('z', '老师'); // 报错
复制代码

用约束的视角来看待类型计算会容易不少

交叉类型 Intersection Type 一般由 & 运算符链接两个类型得出来的,如 A & B 的意思是既要知足 A 的约束,也要知足 B 的约束 (同时知足)

实例参考:

type Admin = { permission: 100 };
type User = { permission: number, name: string };

function systemReboot(user: User & Admin) {
    if (user.permission < 100) {
        throw new Error('systemReboot error: permission deny');
    } else {
        /** $ sudo reboot **/
    }
}

systemReboot({
    permission: 1, // 这里不知足 Admin 的约束 报错哦
    name: '普通用户'
});

systemReboot({
    permission: 100, // 能够 ~
    name: '管理员用户'
});

// 有了交叉类型咱们便没必要定义 AdminUser
type AdminUser = { permission: 100, name: string  };
// 取而代之的是 (避免了重复声明、尽量的利用现有元素来构造新的类型)
type AdminUser = Admin & User;
复制代码

看完上面的例子,估计不少人都会想到,能不能定义偶数这种类型?以目前 ts 的能力来看,如今还不具有基本类型的动态拆解能力,或许将来会有,可是 ts 如今能够作到对象的动态拆解/抽象哦,后文会详细描述。

4、构造 never 类型

了解联合和交叉类型后,聪明的你也许已经发现了相似这样的类型表达式:

type WTF = 'A' & 'B';
复制代码

既是字符串 'A' 又是字符串 'B' 的「薛定谔的值」?显然,js 里是不存在这样的值的

经过 VSCode 咱们能够看到这里的 WTF 类型是 never 其含义是 any 的对立面,即「什么值都不兼容」或者「没有」:

  1. any: 无约束 => 什么值均可以兼容
  2. never: 无穷强的约束 => 什么值都不兼容

如下是对「什么值都不兼容」的代码说明

let n: never;
n = e;
n = 'e'
n = {};
n = () => {};
n = n;
// never 跟任何类型都不兼容,除了它自己
复制代码

💡💡💡 never 并不是一无可取,在后文的一些高级用法里 never 很常见。


关注【IVWEB社区】公众号查看最新技术周刊,今天的你比昨天更优秀!


5、利用 extends 拓展类型

extends 拓展了 ES 原生的 extends,在 ts 的语境下,A extends B 意思是既要 A 继承自 B,其做用相似于 & :

interface AdminUser extends User { permission: 100 };
interface User { permission: number, name: string };

function systemReboot(user: AdminUser) {
    if (user.permission < 100) {
        throw new Error('systemReboot error: permission deny');
    } else {
        /** $ sudo reboot ... **/
    }
}

systemReboot({
    permission: 1, // 这里不知足 Admin 的约束 报错哦
    name: '普通用户'
});

systemReboot({
    permission: 100, // 能够 ~
    name: '管理员用户'
});
复制代码

此外,extends 还能够用来约束泛型的范围:

interface HasName {
    name: string;
}

// 这里的意思是 T 做为泛型的话首先要知足 HasName
function sayObjName<T extends HasName>(obj: T) {
    console.log(obj.name); // 不会报错
}

sayObjName({ name: 'eczn' });

sayObjName({});
// 类型不和报错,
// 由于在这里 T 的类型是 {}
// 它并不知足 HasName 的约束
复制代码

6、构造对象索引

在实际代码运行的过程当中,咱们老是有这样的一种需求

有这样的一种对象 Map:其键是某个惟一 Key,它对应的值是这个 Key 表明的对象

也就是说须要定义「对象的键和值」

在这种状况下,咱们能够为这种「对象」声明它的「索引类型」以达到咱们的要求:

interface User {
    uid: string;
    name: string;
}

interface ObjMap {
    // 这意思是对象键名类型为 string 其对应的值类型为 User
    [uid: string]: User;
    // string => User
}

const map: ObjMap = {
    'eczn': { uid: 'eczn', name: '喵呜' },
    'eeee': 'ggg' // 不知足,报错
}
复制代码

💡💡💡 若是你喜欢用 Array.prototype.reduce 规约数组的话,对象索引会用的比较多

7、利用 keyof 构造键名联合

keyof 是 ts 提供的类型运算符,用于取出对象类型的键名联合,返回的结果是一个联合类型:

interface Person {
    name: string;
    age: number;
    sex: 0 | 1; // 0 表明女士;1 表明男士
}

type KeyOfPerson = keyof Person;
// 'name' | 'age' | 'sex'
// 这里 KeyOfPerson 的意思是多是 'name' 多是 'age' 多是 'sex'

const personKey: KeyOfPerson = 'xxx';
// ^^^^^ 报错 xxx 并非 Person 键
复制代码

利用 keyof,能够很容易的遍历一个对象的字段,并在原对象的基础上生成新的对象:

// 下面的这个类型会把 T 上面的字段对应的值所有设置为 number
type ObjToNum<T> = {
    [key in keyof T]: number;
}

type Person = {
    name: string;
    address: string;
}

type Test = ObjToNum<Person>;
// Test = { name: number, address: number }
复制代码

ObjToNum 中 key in keyof T 的意思是说, 遍历 keyof T 里的元素做为 key, 将这些 key 做为键,并将这些键所对应的值类型设置为 nunber。

考虑到 key in keyof T,中的 keyof T 能够是任意的联合类型或字面量,所以能够很容易的写出相似下面这样的类型 JustNameAge:

// HasNameAge 用于约束泛型
interface HasNameAge {
    name: any,
    age: any
}

// 将 T 里面的 name 和 age 单独挖出来做为新类型
// (这个新类型是 T 的子集)
type JustNameAge<T extends HasNameAge> = {
    // key 是变量 T[key] 也就很容易理解了
    [key in 'name' | 'age']: T[key]

    // 固然, 最好这样写 (减小冗余)
    // [key in keyof HasNameAge]: T[key]
}

// Test1 => { name: string, age: number }
type Test1 = JustNameAge<{
    name: string,
    age: number,
    sayName: () => void
}>;

// 下面这个会报错
// 由于其泛型入参不知足 HasNameAge 约束
type Test2 = JustNameAge<{
    name: string
}>;
复制代码

8、构造条件类型 Conditional Types

有时候,咱们须要去除一个对象的函数项 ... 这里可能须要通常的编程语言里面的 if 判断来进行类型抽象。

首先,我先声明一些基础类型:

// 咱们的问题是:
// 如何将 ABC 中的函数项去除,使其变成 type ABC2 = { a: 1 } ?
type ABC = { a: 1, b(): string, c(): number };

// 若是一个值知足这个约束,则这个值为一个函数
type AnyFunc = (...args: any[]) => any;
// (也许你也猜到了,我用它来作形如 T extends AnyFunc 的操做)
复制代码

下一步,咱们利用 ? 并结合 extends 作处理:

// 构造 Test1
// Test1 = { a: "a"; b: never; c: never; }
type Test1 = {
    // 这里的意思是 ABC[K] 若是知足 AnyFunc 则取出 K,否则取的是 never
    [K in keyof ABC]: (
        ABC[K] extends AnyFunc ? never : K
        // ^^^ 注意这里拿的是 K
    )
}

// 而后构造 Test2
// Test1[keyof ABC]
// = Test1['a' | 'b' | 'c']
// = 'a' | never | never 
// = 'a'
// ^^^ 注意这里,对于任意类型 A,never | A 最后等于 A
type Test2 = Test1[keyof ABC];


// 而后咱们在 Test2 的基础上进行最后一步处理获得 Test3:
// Test3 = { a: 1 }
type Test3 = {
    [K in Test2]: ABC[K]
}
复制代码

把上面的推导过程整理一下,能够获得 GetStaticFor 用于抽取某对象类型的非函数项:

// 这里的思想是取出静态项所对应的 keys
type GetStaticKeysFor<T> = {
    [K in keyof T]: T[K] extends AnyFunc ? never : K
}[keyof T];

// 而后再利用这个 keys 去遍历原对象来取出对应的键值
type GetStaticFor<T> = {
    [K in GetStaticKeysFor<T>]: T[K]
}
复制代码

9、使用 infer 进行 extends 推断

有时候,咱们须要将泛型「挖出来」,好比咱们须要获取到 Promise 类型里蕴含的值,能够利用 infer 这样使用:

type PromiseVal<P> = P extends Promise<infer INNER> ? INNER : P;

type PStr = Promise<string>;

// Test === string
type Test = PromiseVal<PStr>;
复制代码

此外,infer 只能跟在 extends 的后面出现,由于只有 extends 的语境下,才能体现 infer 的语义:动态地给类型的某个结构命名 以便在后续的 TRUE 分支里面使用。

Array 也能够:

type ArrayVal<P> = P extends Array<infer INNER> ? INNER : P;

// Test ==> string | number
type Test = ArrayVal<[string, number]>;
复制代码

10、本篇末

本篇主要讲述的是如何构造类型抽象以便描述/生成更多的类型,如下是 Checklist:

  1. 掌握本篇当中描述的各类类型抽象方法
  2. 能熟练使用范型、熟练的查看其余人写的类型定义
  3. 经过搭配不一样简单抽象来构造更复杂的抽象
  4. 利用类型抽象减小业务代码中类型标注的冗余性,减小重复工做

本文的下一篇是「工程化和运行时、TypeScript 编程内参(三)」,敬请期待

相关连接: 约束即类型、TypeScript 编程内参(一)

相关文章
相关标签/搜索