TypeScript强大的类型操作

166 阅读8分钟
    /**
        @date 2021-12-12
        @description TypeScript Type Manipulation
    */

壹(序)

TypeScript Handbook中有这样一章:Type Manipulation

这一章介绍了ts强大的类型系统,手册第一句话是这样:

TypeScript’s type system is very powerful because it allows expressing types in terms of other types.

怎么理解这句it allows expressing types in terms of other types.呢?

我的理解就是,ts的类型系统就是可以看作一门图灵完备的语言的,类似正常的语言,它也支持输入输出,可以做条件判断。特殊之处在于,ts的类型系统输入输出的就是type,所以说能够根据输入的type输出新的type。

比如我们在js中写一个函数sum用于计算两者之和:

function sum(a, b) {
    return a + b;
}

函数sum的输入就是两个值a和b,输出的是a与b之和。

那么在ts的类型系统中,我们同样可以提供一个输出,再输出想要的:

type isNumber<T> = T extends number ? true : false;

如上我们实现了一个 type 用于判断输入的 type 是否是 number,返回的也是 type。

从上面的小栗子我们就能初窥ts类型系统的强大,强大的不是这个栗子,而是它做到的事情,既然我们可以实现一个简单的类型推导,那么我们就能实现困难的类型推导,就像其他语言,只要语言内部实现了如输入输出、条件语句、循环语句之类的功能,那么我们就能够根据这些简单的功能去实现强大的需求,关键就在于我们自身是否足够强大了。

贰(泛型)

在我看来,上面isNumber的实现里,最基本也是最重要的,是传入的那个 T,这是起源之物,没有传入的 T,我们也能够做一个类型是否是 number 的判断,但是就只能做一种类型的判断了,就不能叫 isNumber,得叫 isXXXNumber。这与其他语言也是类似的,在其他能够实现函数的语言中,有一种函数称为纯函数,意思就是此类函数的输入与输出一定是固定的,不会有外部影响导致同样的两次输入但是不同的两次输出。

比如上面的 sum 函数,就是一个纯函数,我即便计算一万次sum(1, 1),输出都会是 2,所以我说输入是及其重要的。

ts类型系统也是这样,而类型系统的输入被称为Generics(泛型)isNumber的 T 就是一个泛型,我们根据传进来的这个泛型 T 做一个条件判断,然后返回得到的结果。

所以如何理解泛型?很简单,就可以看作一个函数的输入。

同其他语言的输入一样,类型系统也是可以有多个输入的,比如实现一个类型,传入三个泛型,根据第一个输入是否是true来选择返回第二个输入还是第三个输入:

type If<C, T, F> =  C extends true ? T : F;

叁(keyof)

js 中有 Object.keys() 能够获取当前对象的所有可枚举属性,ts 中有 keyof 操作符,用于获取对象中所有可枚举属性,比如

interface Person {
    name: string;
    age: number;
}

type P = keyof Person;

这里的 P 是等价于'name' | 'age'的。

肆(typeof)

js中可以使用 typeof 获取变量的类型,ts中也定义了 typeof 操作符,用来获取变量或属性的type,比如:

const str = 'test';
type StrType = typeof str; // 'test'

由于上面使用的是 const 声明变量,所以得到的类型是固定的,如果使用 let 则会得到 string

let str = 'test';
type StrType = typeof str; // 'string'

typeof 不仅仅用于基本数据类型,也能作用于 objectarray,更大的作用是对于函数的推断:

function foo() {
  return {
    name: 'E1e',
    age: 18,
  };
}

type FooType = typeof foo; // () => { name: string; age: number; }

不过无法用于表达式:

function foo() {
  return {
    name: 'E1e',
    age: 18,
  };
}

type FooType = typeof foo(); // error

伍(Indexed Access Types)

ts中还能使用索引来访问类型,得到 arrayobject 中属性的类型,就如同js中,我们可以使用 [] 访问对象属性,ts中也能使用 [],不过得到的是类型,当然使用的也需要是type:

type Person = {
    name: string;
    age: number;
}
type Name = Person['name']; // string
type Age = Person['age']; // number

类似js获取属性,我们也可以使用一个类型变量去获取类型,比如上面的 type Age = Person['age'];,我们可以将 'age' 声明成一个类型,再通过这个类型去获取:

type AgePropName = 'age';
type Age = Person[AgePropName]; // number

还可以使用联合类型获取多个属性的类型:

type Union = Person['name' | 'age']; // string | number

对于 array,我们可以使用 number表示索引,再结合 typeof 来获取 array 内部的类型:

const arr = [
  {
    name: 'E1e',
    age: 18,
  },
  {
    name: 'wy',
    age: 17,
  },
  {
    name: 'E1e_wy',
    age: 25,
  },
];

type Arr = typeof arr[number]; // { name: string; age: number; }

需要注意的是,类型系统中就是使用类型的,如果使用了js中的值是会报错的,需要结合 typeof 一起使用:

const nameKey = 'name';
// type Name = Person[nameKey]; // error
type Name = Person[typeof nameKey]; // string

陆(Conditional Types)

ts类型系统中也是可以使用条件判断语句的,不过只支持三元运算符的条件判断,并且需要配合 extends 操作符使用,虽然简陋了一点,不像js等常规语言那样可以使用 if 语句,但是毕竟ts对于js来说只是: A Typed Superset of JavaScript

使用很简单,想一想三元运算符:condition ? true : false

type Person = {
  language: string;
}
type Cat = {
  language: 'meow'
}
const canUseTools = true;
type Me = typeof canUseTools extends true ? Person : Cat; // { language: string; }

上面使用条件判断判定出 Me 是一个 Person

最重要的使用是结合泛型一起使用,用于做些高级一点的类型操作:

type Person = {
  feetNum: 2;
  canUseTools: true;
}
type Cat = {
  feetNum: 4;
  canUseTools: 'maybe';
}

type WhatThis<T extends { language: any }> = T['language'] extends 'meow' ? Cat : Person;

type Me = {
  language: string;
}
type LittleCat = {
  language: 'meow';
}

type GetMe = WhatThis<Me>
type GetCat = WhatThis<LittleCat>

上面我通过 language 的不同类型,得到 PersonCat 的类型

柒(Mapped Types)

映射类型操作,其实可以看作遍历,比如想要将一个类型中的所有属性变为 string

type Person = {
  name: string;
  age: number;
}

type PropBooleanFormatter<T> = {
  [P in keyof T]: boolean;
}

type PersonBoolean = PropBooleanFormatter<Person>; // { name: boolean; age: boolean; }

在遍历过程中,还可以加上一些额外的操作,比如把所有属性设置为 readonlyoptional

type MyReadonly<T> = {
    readonly [P in keyof T]: T[P];
}
type MyOptional<T> = {
    [P in keyof T]?: T[P];
}

同样的,也能去除 readonlyoptional 属性,在前面加上 - 即可:

type NoReadonly<T> = {
    -readonly [P in keyof T]: T[P];
}
type NoOptional<T> = {
    [P in keyof T]-?: T[P];
}

捌(Template Literal Types)

ES6中新增了模板字符串,将变量与字符串结合在一起使用,同样的,ts中也能使用模板字符操作,与js中类似,不过是作用与类型系统:

type Hello = 'hello';
type HelloWorld = `${Hello} world`;

将模板字符操作与联合类型结合起来使用,可以减少很多操作:

type PersonGender = 'man' | 'woman';
type AnimalGender = 'male ' | 'female';

type GenderIds = `${PersonGender | AnimalGender}_id`;

同时,利用模板字符操作,可以基于已有属性增加基于该属性的其他属性,比如为某个属性增加一个 onXXXChanged 事件:

type Person = {
  name: string;
  age: number;
}

type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => any): any;
};

type NewModalProps = Person & PropEventSource<Person> 

const person: NewModalProps = {
  name: 'E1e',
  age: 18,
  on: () => {}
}

person.on('nameChanged', () => {});
person.on('ageChanged', () => {});
person.on('maleChanged', () => {}); // error: Argument of type '"maleChanged"' is not assignable to parameter of type '"nameChanged" | "ageChanged"'

玖(Loop Types)

ts的类型系统是可以实现循环语句的,并不是上面 Mapped Types 中的遍历数组或对象,而是类似 for 循环的循环语句,但是作为js的超集,必定不可能实现 for 循环或 while 循环之类的语句,因为会与js冲突,不过除了这些循环,还可以使用递归实现循环操作:

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends Function ? T[P] : T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

上面使用递归实现了一个深层readonly类型

拾(infer)

infer 可以用来推断类型中的类型,比如想要获取一个函数的入参类型:

type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never

获取入参的类型,就可以直接使用 infer 进行推断,ts类型系统会自动的推断出参数类型,然后返回;

需要注意的是:infer 需要结合条件判断使用。

在我看来,infer 就是利用了ts内部的推断机制,让使用者可以直接推断出未知的值并返回,而不需要去做重重判断,去处理复杂的数据结构或者说是不需要自己去维护一些数据结构,而是让ts的推断机制帮忙,但是需要结合条件判断,条件判断通过则表示推断机制成功的推断出你想要的类型了,失败则表示没有推断出来,所以一般在失败时返回 never

以上是我对ts类型操作以及类型系统的一些浅显理解,大部分还是官方文档中关于类型操作的描述,所以如果有兴趣的话建议仔细阅读文档;

不过这些只是一些基础的知识,想要真正的利用起来,或者做一些练习,推荐去type-challenges中做一些题目,或者去看ts内部的一些工具类型

最后想说的是,ts并不仅仅是一个js类型检查工具,也是一门强大的语言,虽然可能有很多特性并不像js那样需要都去理解,但是基本的理解也是必须的。