ts的泛型与类型体操
一、类型
什么是类型
是编程语言提供的对不同内容的抽象:
- 不同类型变量占据的内存大小不同:boolean 类型的变量会分配 4 个字节的内存,而 number 类型的变量则会分配 8 个字节的内存,给变量声明了不同的类型就代表了会占据不同的内存空间。
- 不同类型变量可做的操作不同:number 类型可以做加减乘除等运算,boolean 就不可以,复合类型中不同类型的对象可用的方法不同,比如 Date 和 RegExp,变量的类型不同代表可以对该变量做的操作就不同。
类型安全
保证对某种类型只做该类型允许的操作
类型检查
保证类型安全的机制,可以在运行时做,也可以运行之前的编译期做。这是两种不同的类型,前者叫做动态类型检查,后者叫做静态类型检查
动态类型检查 在源码中不保留类型信息,对某个变量赋什么值、做什么操作都是允许的,写代码很灵活。但这也埋下了类型不安全的隐患,比如对 string 做了乘除,对 Date 对象调用了 exec 方法,这些都是运行时才能检查出来的错误。
静态类型检查则是在源码中保留类型信息,声明变量要指定类型,对变量做的操作要和类型匹配,会有专门的编译器在编译期间做检查。
动态类型只适合简单的场景,对于大项目却不太合适,因为代码中可能藏着的隐患太多了,万一线上报一个类型不匹配的错误,那可能就是大问题。
而静态类型虽然会增加写代码的成本,但是却能更好的保证代码的健壮性,减少 Bug 率。
二、TypeScript的类型编程
ts是js的超集
TypeScript 给 JavaScript 增加了一套静态类型系统,通过 TS Compiler 编译为 JS,编译的过程做类型检查。它并没有改变 JavaScript 的语法,只是在 JS 的基础上添加了类型语法,所以被叫做 JavaScript 的超集。
静态类型编程语言都有自己的类型系统,从简单到复杂可以分为 3 类:
1.简单类型系统
为保证类型安全,对变量、函数、类等声明类型,比如:
function add(a:number, b: number):number;
function add(a:string, b: string):string;
function add(a: any, b: any) {
return a + b;
}
比较死板,得申请两次,如果类型能传参数就好了,传入 int 就是整数加法,传入 double 就是浮点数加法。所以,就有了第二种类型系统。
2.支持泛型的类型系统
泛型的英文是 Generic Type,通用的类型,它可以代表任何一种类型,也叫做类型参数。
声明时把会变化的类型声明成泛型(也就是类型参数),在调用的时候再确定类型。
function add<T>(a: T, b: T): T {
return (a + b) as T
}
但是这种类型系统的灵活性对于 JavaScript 来说还不够,因为 JS 是弱类型语言,变量的类型是可以随意变化的,所以就有了第三种类型系统。
3.支持类型编程的类型系统
对传入的类型参数(泛型)做各种逻辑运算,产生新的类型,这就是类型编程。
比如,我们要返回对象某个属性值的函数
function add<T extends number | string>(a: T, b: T): T {
if (typeof a === 'number' && typeof b === 'number') {
return (a + b) as T;
}
if (typeof a === 'string' && typeof b === 'string') {
return (a + b) as T;
}
throw new Error('Invalid arguments');
}
function getPropertyValue<T extends Record<string, any>, Key extends keyof T>(obj: T, key: Key): T[Key] {
return obj[key];
}
这里的 keyof T、T[Key] 就是对类型参数 T 的类型运算。
现在 TS 的类型系统是图灵完备的,JS 可以写的逻辑,用 TS 类型都可以写。
但是很多类型编程的逻辑写起来比较复杂,因此被戏称为类型体操。
三、ts的类型运算
静态类型系统的目的是把类型检查从运行时提前到编译时,那 TS 类型系统中肯定要把 JS 的运行时类型拿过来,也就是 number、boolean、string、object、bigint、symbol、undefined、null 这些类型,还有就是它们的包装类型 Number、Boolean、String、Object、Symbol。复合类型方面,JS 有 class、Array,这些 TypeScript 类型系统也都支持
但是又多加了三种类型:元组(Tuple)、接口(Interface)、枚举(Enum),字面量类型,以及4种特殊类型:void、never、any、unknown
ts额外的类型
1.元组
元组(Tuple)就是元素个数和类型固定的数组类型
type Tuple = [number, string];
数组类型是指任意多个指定类型的元素构成的,比如 number[]、(number | string)[]、Array
元组则是数量固定,且每个元素的类型固定的元素构成的,比如 [1, true, 'guang']。
2.接口
接口(Interface)可以描述对象、函数、构造器
2.1 对象
interface IPerson {
name: string;
age: number;
}
class Person implements IPerson {
name: string;
age: number;
}
const obj: IPerson = {
name: 'guang',
age: 18
}
2.2 函数
interface SayHello {
(name: string): string;
}
const func: SayHello = (name: string) => {
return 'hello,' + name
}
2.1 构造器
interface PersonConstructor {
new (name: string, age: number): IPerson;
}
function createPerson(ctor: PersonConstructor):IPerson {
return new ctor('guang', 18);
}
对象类型、class 类型在 TypeScript 里也叫做索引类型,也就是索引了多个元素的类型的意思。对象可以动态添加属性,如果不知道会有什么属性,可以用可索引签名({[key:string]: any}):
interface IPerson {
[prop: string]: string | number;
}
const obj:IPerson = {};
obj.name = 'guang';
obj.age = 18;
总之,接口可以用来描述函数、构造器、索引类型(对象、class、数组)等复合类型
3.枚举
枚举(Enum)是一系列值的复合:
enum Transpiler {
Babel = 'babel',
Postcss = 'postcss',
Terser = 'terser',
Prettier = 'prettier',
TypeScriptCompiler = 'tsc'
}
const transpiler = Transpiler.TypeScriptCompiler;
4.字面量
此外,TypeScript 还支持字面量类型,也就是类似 1111、'aaaa'、{ a: 1} 这种值也可以做为类型。
4.1 字符串的字面量
字符串的字面量类型有两种:
1.普通的字符串字面量,比如 'aaa'
- 模版字面量,比如
aaa${string},它的意思是以 aaa 开头,后面是任意 string 的字符串字面量类型
所以想要约束以某个字符串开头的字符串字面量类型时可以这样写:
function fn(str: `#${string}`) {
}
fn('aaa');
fn('#aaa');
5.四个特殊类型
还有四种特殊的类型:void、never、any、unknown:
- never 代表不可达,比如函数抛异常的时候,返回值就是 never。
- void 代表空,可以是 undefined 或 never。
- any 是任意类型,任何类型都可以赋值给它,它也可以赋值给任何类型(除了 never)。
- unknown 是未知类型,任何类型都可以赋值给它,但是它不可以赋值给别的类型。
- any 和 unknown 都代表任意类型
- any 禁用了类型检查;unknown 仍然保持类型安全
- any 因为绕过了类型检查,所以它赋值给任何类型的变量,也可以从任何类型的变量赋值过来(除了 never);unknown 只能接收任意类型的值,不能对 unknown 类型的值进行操作
- 类型体操中经常用 unknown 接受和匹配任何类型,而很少把 any 赋值给某个类型变量
- any 会失去 TypeScript 带来的类型安全,容易引入运行时错误
类型的属性
除了描述类型的结构外,TypeScript 的类型系统还支持描述类型的属性,比如是否可选?,是否只读readonly等:在属性前添加-则取表示相反属性
interface IPerson {
readonly name: string;
age-?: number;
}
type tuple = [string, number?];
类型运算
1.条件:extends?
TypeScript 里的条件判断是 extends ? :,叫做条件类型(Conditional Type)比如:
// 类型参数的运算
type isTwo<T> = T extends 2 ? true: false;
type res = isTwo<1>;
type res2 = isTwo<2>;
2.推导:infer
如何提取类型的一部分呢?答案是 infer。所以我更愿叫 infer 为提取
比如提取元组类型的第一个元素:
type First<Tuple extends unknown[]> = Tuple extends [infer T,...infer R] ? T : never;
type res = First<[1,2,3]>;
3.联合:|
联合类型(Union)类似 js 里的或运算符 |,但是作用于类型,代表类型可以是几个类型之一。
type Union = 1 | 2 | 3;
4.交叉:&
交叉类型(Intersection)类似 js 中的与运算符 &,但是作用于类型,代表对类型做合并。
type ObjType = {a: number } & {c: boolean};
同一类型可以合并,不同的类型没法合并,会被舍弃
常见的场景:
当 组件A 大多数属性都继承 组件B 的属性,但是其中onChange方法参数类型不同,这个时候必须先将 组件B 的 onChange 排除,再新增 组件A 的onChange定义
5.类型映射
对象、class 在 TypeScript 对应的类型是索引类型(Index Type),那么如何对索引类型作修改呢?
答案是通过 in 作 类型映射。
type MapType<T> = {
[Key in keyof T]?: T[Key]
}
keyof T 是查询索引类型中所有的索引,叫做索引查询。
T[Key] 是取索引类型某个索引的值,叫做索引访问。
in是用于遍历联合类型的运算符。
比如我们把一个索引类型的值变成 3 个元素的数组:
type MapType<T> = {
[Key in keyof T]: [T[Key], T[Key], T[Key]]
}
type res = MapType<{a: 1, b: 2}>;
四、类型体操
类型体操(TypeScript 类型操练)指的是在 TypeScript 中编写复杂的类型以实现更精准的类型检查和类型提示(推导)。
1.模式匹配做提取
通过
extends对类型参数做模式匹配通过
infer声明的局部类型变量(准确来讲不是变量,而是类型)并保存如果匹配,就能从该局部变量里拿到提取出的类型
2.重新构造做变换
通用
extends做类型 约束 或者一些 拆分,再
- 通过
infer提取出来,- 还有其他手段做一些 过滤 和 变换,比如:
- 通过
...解构- 通过
in遍历,再通过keyof获取索引,再通过T[Key]获取值,- 通过
?可选属性,-?取消可选属性readonly只读属性,-readonly取消只读属性- 通过
&交叉类型,通过|联合类型,通过as类型断言- 通过
as const字面量类型,注意as const只能对常量使用,不能对变量使用
3.递归复用做循环
不固定层级,不固定数量的情况下,根据条件做递归
4.数组长度做计算
通过数组类型的构造和提取,然后取长度的方式来实现数值运算
5.联合分散做简化
联合类型的特性:分布式,每个类型都是相互独立的,TypeScript 对它做了特殊处理。
在 字符串 里使用 联合类型的索引访问时,每个类型都会单独传入计算一次
当 条件类型
extends左边 是 联合类型时,每个类型都会单独执行一次条件判断以上两种,最后每个类型的计算结果再合并成新的联合类型
6.其他特性
一些类型的特性还是要记一下。在判断或者过滤类型的时候会用到:
- any 类型与任何类型的交叉都是 any,也就是 1 & any 结果是 any,可以用这个特性判断 any 类型。
- never 作为类型参数出现在条件类型左侧时,会直接返回 never。判断 never 类型
- any 作为类型参数出现在条件类型左侧时,会直接返回 trueType 和 falseType 的联合类型。
- 元组类型也是数组类型,但 length 是数字字面量,而数组的 length 是 number。可以用来判断元组类型。
- 函数参数处会发生逆变,可以用来实现联合类型转交叉类型。
- 可选索引的索引可能没有,那 Pick 出来的就可能是 {},可以用来过滤可选索引,反过来也可以过滤非可选索引。
- 默认推导出来的不是字面量类型,加上
as const可以推导出字面量类型,但带有readonly修饰,这样模式匹配的时候也得加上readonly才行。
其它
逆变、协变、双向协变与不变
ts就是为了做类型检查的,不同的类型只能使用该类型的属性和方法,但是为了增加类型系统的灵活性,ts设计了父子类型的概念
父子类型之间自然应该能赋值,也就是会发生型变(variant)。型变(类型改变)分两种:协变和逆变
非父子类型之间不能发生型变,也就是不变的特性
通过结构,更具体的那个是子类型。这里的 Guang 有 Person 的所有属性,并且还多了一些属性,所以 Guang 是 Person 的子类型。
协变
子类型可以赋值给父类型,简单但不是很准确来讲就是:更多的可以赋值给少的
逆变
函数赋值的时候函数参数的性质,参数的父类型可以赋值给子类型,更少的赋值给更多的,作为参数也是没有问题
双向协变是不开启
strictFunctionTypes的话,函数参数子类型也可以赋值给父类型,ts 2.x之后默认开启strictFunctionTypes
内置的高级类型
Parameters用于提取函数类型的参数类型ConstructorParameters用于提取构造器参数的类型。InstanceType提取构造器返回值的类型ThisParameterType提取 this 对象OmitThisParameter方法类型中移除 this 参数ReturnType用于提取函数类型的返回值类型。Record创建对象Readonly只读Partial可选Required必选Pick提取属性子集Omit排除属性子集Extract提取类型子集Exclude排除类型子集Awaited解析和提取 Promise 类型的结果类型NonNullable用于判断是否为非空类型,也就是不是 null 或者 undefined 的类型的Uppercase、Lowercase、Capitalize、Uncapitalize分别实现大写、小写、首字母大写、去掉首字母大写的