/**
@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
不仅仅用于基本数据类型,也能作用于 object
及 array
,更大的作用是对于函数的推断:
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中还能使用索引来访问类型,得到 array
或 object
中属性的类型,就如同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
的不同类型,得到 Person
和 Cat
的类型
柒(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; }
在遍历过程中,还可以加上一些额外的操作,比如把所有属性设置为 readonly
或 optional
:
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
}
type MyOptional<T> = {
[P in keyof T]?: T[P];
}
同样的,也能去除 readonly
或 optional
属性,在前面加上 -
即可:
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那样需要都去理解,但是基本的理解也是必须的。