类型体操
TypeScript 介绍
TypeScript 由微软开发的自由和开源的编程语言。
TypeScript 是 JavaScript 的一个超集,TypeScript 本身是没有改变JavaScript 语法的,而只是 给 JavaScript 增加了一套静态类型系统,而通过 TS Compiler 编译为 JS,编译的过程做类型检查。
什么是类型体操
除了TypeScript 以外,一些编程语言本身就自带类型系统,天然支持类型定义和检查。例如 Java、C++ 等。 但是只有 TypeScript 的类型编程被叫做类型体操,为什么其他语言没有呢?
类型系统从简单到复杂可以分为三类:简单类型系统、支持泛型的类型系统、支持类型编程的类型系统
简单类型系统
变量、函数、类等都可以声明类型,编译器会基于声明的类型做类型检查,类型不匹配时会报错。
const a: number = 1
const add = (a:number,b:number):number =>{
return a+b
}
支持泛型的类型系统
泛型的英文是 Generic Type,通用的类型,可以代表任何一种类型,也叫做类型参数。
type AddType<T> = (a: T) => T
type fun = AddType<number>
声明时把会变化的类型声明成泛型(也就是类型参数),在调用的时候再确定类型。
虽然增加一点灵活性,但是 对于JavaScript 来说还不够。例如下面这种情况:
function getPropValue<T>(obj: T, key) {
return obj[key];
}
存在问题:即使拿到了T,但是也无法获取它的属性和属性值。
支持类型编程的类型系统
传入的类型参数(泛型)做各种逻辑运算,产生新的类型,这就是类型编程。
function getPropValue<
T extends object,
Key extends keyof T
>(obj: T, key: Key): T[Key] {
return obj[key];
}
TypeScript 不仅仅只是为JavaScript 增了一套类型系统而已,而对于它自身而言,它属于是图灵完备 的静态编程语言。
结论:类型体操就是充分利用 TS 提供的一系列计算机指令,来进行类型编程。
所以对待TS,不应该只把他当作一门类型定义系统来看待,而应该把它当作一门完整的编程语言来学习。
- 类型的声明定义
- 条件判定和遍历循环
- 特殊的语法概念
- 编程的一些技巧
类型基础
类型定义
-
基本类型:number、boolean、string、bigint、symbol、undefined、null
-
复合类型: Class、Array、object、元组(Tuple) 、接口(Interface)、枚举(Enum)
-
特殊类型: void、never、any、unknown
undefined & null
在 TS 中 两者都是有具体含义的,没有开启 strictNullChecks检查的情况下,会被视作其他类型的子类型。(never 除非)
const tmp1: null = null;
const tmp2: undefined = undefined;
const tmp3: string = null; // 仅在关闭 strictNullChecks 时成立
const tmp4: string = undefined;
object
表示对象,但在Ts 中存在 object Object 和{} , 这三者都表示一个空对象。
- Object 表示包含了所有的类型。
// 对于 undefined、null、void 0 ,需要关闭 strictNullChecks const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void 0;
const tmp4: Object = 'hello';
const tmp5: Object = 599;
const tmp6: Object = { name: 'hello' };
const tmp7: Object = () => {};
const tmp8: Object = [];
- object:表示的就是除基本类型以外的类型,例如 数组 、对象、函数和类。
const tmp1: object = 'hello'; // 不成立, const tmp2: object = 599; // 不成立,
const tmp3: object = () => {};
const tmp4: object = [];
const tmp5: object = { name: 'hello' };
class a {}
const tmp6:object = a
- {} 可以认为就是个对象字面量类型,但它的内部无属性定义。 类似于
new Object,使用它声明的变量是无法进行任何赋值操作的。
const tmp1: {} = { name: 'hello' };
tmp1.age = 18; // X 类型“{}”上不存在属性“age”。
总结:
-
在任何时候都应该避免使用 Object , {} ,使用它没有任何实际意义。
-
在不确定变量属于哪种复合类型的时候,可以用
object。如果知道是对象结构 ,就建议使用Record<string,any>来声明类型。
元组(Tuple)
元组表示的元素个数和类型固定的数组类型
type Tuple = [number, string]; // 元组类型
type arry = Array<string|number> // 数组类型
特殊类型:
- never 代表虚无,比如函数抛异常的时候,返回值就是 never。
- void 代表空,返回值是 undefined。
- any 是任意类型,任何类型都可以赋值给它,它也可以赋值给任何类型(除了 never)。
- unknown 是未知类型,任何类型都可以赋值给它,但是它不可以赋值给别的类型。
类型工具
类型工具主要分为两类:创建类型 和 类型保护
创建类型:联合交叉、索引类型、映射类型、类型查询
类型保护:条件类型
联合交叉
交叉类型使用 &, 按位与运算符。
联合类型使用 |,按位或运算符。
交叉类型
对于声明的对象类型,使用交叉类型,最终的效果是合并
interface IName {
name:string
}
interface IAge {
age:number
}
// interface 定义
interface Lisi extends IName,IAge {
isMan: boolean
}
// type 定义
type Lisi = IName & IAge & { isMan: boolean }
对于不同的基础类型进行交叉,就会得到never,表示不存在的类型
type List = string & number // never
联合类型
联合类型和交叉类型的区别就是,联合类型只需要符合成员之一即可,而交叉类型需要严格符合每一位成员。
type a = (1 | 2 | 3) & (1 | 2); // 1 | 2
type b = (string | number | symbol) & string; // string
索引类型
索引类型主要包含三个部分::索引签名、索引查询 、 索引访问
索引签名
在接口或类型别名中,快速声明一个键值类型一致的类型结构。
interface A {
[key: string]: string;
}
type B = {
[key: string]: string;
}
索引签名和具体的键值共存
interface A {
age:number;
[key: string]: string;
}
索引查询
使用 keyof 操作符,将对象中的所有键转换为对应字面量类型。
注意:数字的键名转换为字面量的时候,不会变成字符串,仍然保持数字
interface A {
name:string;
2022:boolean;
"2023":number;
}
type B = keyof A // name |2022| ‘2023’
const c:B = 2023 // Type '2023' is not assignable to type 'keyof A'
keyof 不仅适用于对象类型获取它的键,也适用基础类型,获取原型上的键名。
type str = keyof string // number|"toString" | typeof Symbol.iterator ...
type strPro = string['toString']// () => string
索引访问
在JS 中通过 object[expression] 来动态访问属性。 而在TS 中也基本类似的方式,把表达式换成类型
interface A {
[key: string]: number;
}
type AType = A[string]
通过字面量类型来进行索引类型访问
interface A {
name:string;
"2023":number;
2022:string
}
type AType = A[2023]
type BType = A[2022]
type CType = A['name']
搭配索引查询keyof来访问
interface A {
name:string;
"2023":number;
2022:string
}
type AType = A[keyof A] // string|number
映射类型
走向类型编程的第一步
映射类型的主要作用即是基于键名映射到键值类型。
type MapType<T> = {
[Key in keyof T]: T[Key]
}
- keyof T 是查询索引类型中所有的索引,叫做
索引查询。 - T[Key] 是取索引类型某个索引的值,叫做
索引访问。 - in 是用于遍历联合类型的运算符。
总结:映射类型就是把一个集合重新映射生成另一个集合。
使用 as 运算符,实现重映射
type MapType<T> = {
[Key in keyof T as `${Key & string}${Key & string}`]: [T[Key],T[Key]]
}
Key&string
索引类型要求的键是 string、number、symbol , 而keyof T 查询出来索引可能是 string | number | bigint | boolean | null | undefined 的联合类型,所以我们需要使用交叉类型 & ,取 string 作为新的索引。
类型查询
类型查询使用的操作符是 typeof ,这个在js中是最常见的,主要用来检查变量类型。而在Ts中是可以通过它来对当前的变量,表达式进行类型的推导,而且推导是最窄的推导粒度(字面量类型级别)。
const str = "hello";
const obj = { name: "lisi" };
const nullVar = null;
const undefinedVar = undefined
const func = (input: string) => {
return input.length > 10;
}
type Str = typeof str; // "hello"
type Obj = typeof obj; // { name: string }
type Null = typeof nullVar; // null
type Undefined = typeof undefined; // undefined
type Func = typeof func; // (input: string) => boolean
在实际开发中 typeof 同时存在于逻辑层和类型层的,所以为了隔离类型层和逻辑层。类型层中的typeof 后面是不能跟表达式的
条件类型
条件类型用法上看起来有点像 JavaScript 中的三元运算:
条件 ? true 表达式 : false 表达式
对应的TS的写法:
SomeType extends OtherType ? TrueType : FalseType
两者重要的区别 :extends 是根据类型的兼容性来判定是否成立,而不是根据类型是否全等来判定。
最直观的一个兼容性判定案例,父子继承
class Animal {
doAnimalThing(): void {
console.log("I am a Animal!")
}
}
class Dog extends Animal {
doDogThing(): void {
console.log("I am a Dog!")
}
}
type Example1 = Dog extends Animal ? number : string; // number
在TS的类型系统中,兼容性的判定可以根据类型层级链来判定。
type Result = never extends 1
? 1 extends 1 | 2 | 3
? 'name' | '1' extends string
? string extends String
? String extends Object
? Object extends any
? any extends unknown
? unknown extends any
? 8
: 7
: 6
: 5
: 4
: 3
: 2
: 1
: 0
- 配合泛型实现类型约束
在大部分的实际场景中,条件类型主要配合泛型来实现条件约束,
type MessageOf<T> = T["message"];
// Type '"message"' cannot be used to index type 'T'.
type MessageOf<T extends { message: unknown }> = T["message"];
- 配合
Infer关键字和模式匹配来实现类型推导和提取
Infer 关键字在条件类型中用来提取一部分未知类型(unknow)
举例:反转对象键名与键值
type ReverseKeyValue<T extends Record<string, string>> =
T extends Record<infer K, infer V> ?
Record<V & string, K> : never
最终再结合模版字符串,实现类型的推导和提取
举例: 实现字符串提取替换
type ReplaceStr<
Str extends string,
From extends string,
To extends string
> = Str extends `${infer Prefix}${From}${infer Suffix}`
? `${Prefix}${To}${Suffix}` : Str;
type str = ReplaceStr < 'hello world' , 'world' , 'ts' > // "hello ts"
- 分布式条件类型
检查类型为裸类型参数的条件类型称为分布式条件类型。在实例化过程中,分配条件类型会自动分布在联合类型上。 (Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiating)
总结:分布式条件类型属于 TS 中的一项特殊能力,只要满足一定条件就能触发。 满足条件如下:
- 类型参数需要是联合类型
- 是否通过泛型的方式传入。
- 泛型的参数是否为裸类型,就是不能被数组包裹。
// 是否通过泛型的方式传入
type Extract<T, U> = T extends U ? T : never;
type _Extracte1 = Extract<'a'|'b'|'c', 'a'|'b'>;// 'a'|'b'
type _Extracte2 = 'a'|'b'|'c' extends 'a'|'b' ? 'a'|'b'|'c' : never; // never
// 是否为裸类型参数
type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";
type Result1 = Naked<number | boolean>;// "N" | "Y"
type Result2 = Wrapped<number | boolean>; // "N"
类型编程
类型编程本质上就是熟练运用上面这些类型工具,然后再结合一些编程的套路和技巧,最终实现对类型编程。
模式匹配
模式匹配主要就是结合条件类型 extends 对类型参数做匹配,再结合infer 关键词,将局部的类型参数推导出来,如果匹配成功,就能将该类型参数提取出来。
举例:
提取数组首位元素
type ArrayFirst<Arr extends unknown[]> = Arr extends [infer First, ...unknown[]] ? First : never;
type result = ArrayFirst<[1,2,3]>//1
提取函数的参数和返回值
// 提取返回值
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any) => infer R ? R : any;
//提取参数
type paramsType<T extends Function> = T extends (...args: infer P) => any ? P : any;
构造转换
类型的三种声明方式:
- 全局声明:使用类型别名
type - 局部声明 : 使用类型推导
infer,提取局部类型 - 参数声明:使用泛型,类似函数传参,也相当于是局部声明
类型编程中:所有的声明都是不支持修改的,如果需要生成新的类型,就需要重新构造和转换。
举例:
首字母大写
type CapitalizeStr<Str extends string> =
Str extends `${infer First}${infer Rest}`
? `${Uppercase<First>}${Rest}` : Str;
总结:当我们需要生成一个新的类型的时候, 首先通过type 声明一个新的类型别名,通过泛型的方式传入已知的类型,使用infer 提取局部的类型,然后进行重新构造和转换,生成新的类型。
递归遍历
类型编程系统中,本身是不支持循环的,所以想实现循环的效果,就需要借助递归的方式。
使用场景: 当涉及到类型的数量不确定的时候,就需要使用递归了。
举例:
数组反转
type ar = [1,2,3,4,5]
// 数量已知情况,结合模式匹配,使用infer,直接构造转换
type ReverseArr<Arr extends unknown[]> =
Arr extends [infer One, infer Two, infer Three, infer Four, infer Five]
? [Five, Four, Three, Two, One]
: never;
// 数量未知,采用递归
type ReverseArr<Arr extends unknown[]> =
Arr extends [infer First, ...infer Rest]
? [...ReverseArr<Rest>, First]
: Arr;
联合分散
联合类型在类型系统中是比较特殊的,它所具备的特性和实际运用场景也是最多的。
联合类型自身是具备分布式特性,当与条件类型结合使用,才称为分布式条件类型。
举例:
触发分布式特性
type union = 'a'|'b'|'c'
type str = `${union}~~` // "a~~" | "b~~" | "c~~"
type str = `${[union]}~~` // Type '[union]' is not assignable to type 'string | number | bigint | boolean | null | undefined'
触发条件:必须为裸露类型,也就是不能被数组包裹
联合类型与条件类型结合使用的时候,只有条件extends左边的联合类型才会触发分布式特性
举例:
type Union<A, B = A> = A extends A ? { a: A, b: B} : never;
type Result = Union<'a' | 'b' | 'c'>;
结果:
总结
TypeScript 目前已逐步成为前端项目工程中不可或缺的一部分。但在实际的业务开发中,对于TyepScript的运用往往都不会很复杂,更多的是如何去定义类型,不会出现太复杂的类型编程场景。但是,充分理解类型系统能帮助我们更好地理解复杂类型编程的底层原理,也能够让我们获得独立解决各种类型问题的能力。