跳到主要内容

分享

一、类型

什么是类型

是编程语言提供的对不同内容的抽象:

  • 不同类型变量占据的内存大小不同: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'

  1. 模版字面量,比如 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的区别
  1. any 和 unknown 都代表任意类型
  2. any 禁用了类型检查;unknown 仍然保持类型安全
  3. any 因为绕过了类型检查,所以它赋值给任何类型的变量,也可以从任何类型的变量赋值过来(除了 never);unknown 只能接收任意类型的值,不能对 unknown 类型的值进行操作
  4. 类型体操中经常用 unknown 接受和匹配任何类型,而很少把 any 赋值给某个类型变量
  5. 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.模式匹配做提取

  1. 通过 extends 对类型参数做模式匹配

  2. 通过 infer 声明的局部类型变量(准确来讲不是变量,而是类型)并保存

  3. 如果匹配,就能从该局部变量里拿到提取出的类型

2.重新构造做变换

通用 extends 做类型 约束 或者一些 拆分,再

  1. 通过 infer 提取出来,
  2. 还有其他手段做一些 过滤变换,比如:
  • 通过...解构
  • 通过 in 遍历,再通过 keyof 获取索引,再通过 T[Key] 获取值,
  • 通过 ? 可选属性, -? 取消可选属性
  • readonly 只读属性,-readonly 取消只读属性
  • 通过 & 交叉类型,通过 | 联合类型,通过 as 类型断言
  • 通过 as const 字面量类型,注意as const只能对常量使用,不能对变量使用

3.递归复用做循环

不固定层级,不固定数量的情况下,根据条件做递归

4.数组长度做计算

通过数组类型的构造和提取,然后取长度的方式来实现数值运算

5.联合分散做简化

联合类型的特性:分布式,每个类型都是相互独立的,TypeScript 对它做了特殊处理。

  1. 字符串 里使用 联合类型的索引访问时,每个类型都会单独传入计算一次

  2. 条件类型 extends 左边联合类型时,每个类型都会单独执行一次条件判断

  3. 以上两种,最后每个类型的计算结果再合并成新的联合类型

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

示例

内置的高级类型

  1. Parameters 用于提取函数类型的参数类型
  2. ConstructorParameters 用于提取构造器参数的类型。
  3. InstanceType 提取构造器返回值的类型
  4. ThisParameterType 提取 this 对象
  5. OmitThisParameter 方法类型中移除 this 参数
  6. ReturnType 用于提取函数类型的返回值类型。
  7. Record 创建对象
  8. Readonly 只读
  9. Partial 可选
  10. Required 必选
  11. Pick 提取属性子集
  12. Omit 排除属性子集
  13. Extract 提取类型子集
  14. Exclude 排除类型子集
  15. Awaited 解析和提取 Promise 类型的结果类型
  16. NonNullable 用于判断是否为非空类型,也就是不是 null 或者 undefined 的类型的
  17. UppercaseLowercaseCapitalizeUncapitalize 分别实现大写、小写、首字母大写、去掉首字母大写的