欢迎各位多加指正
1. 类型是什么
简单来说,类型就是为编程语言提供不同内容的抽象:
- 不同类型变量占据内存大小
- 不同类型变量可做的操作不同 保证某种类型只允许做该类型的操作叫做类型安全。比如number类型可以进行加减乘除的操作,而boolean类型不可以做这些操作,但是js是一种弱类型语言,即使对boolean类型进行了加减乘除,也不会报错,同时为了保证数据的正确运行,会进行强制类型转换,这就可能导致程序莫名的bug出现。
const num = 1;
const b = true;
const res = num + b; // 2
那么我们怎么避免这样的不可控错误,答案就是类型检查。
保证类型安全方式叫做类型检查,根据类型检查的时间可以分为两种:
- 动态类型:运行时进行检查。
- 静态类型:编译时进行检查。
两种类型各有优劣,动态类型语言比较灵活,但是有类型不安地隐患;静态类型增加了代码编写的难度,但很多bug可以在编译阶段检查出来,从而可以消除不安全隐患。
可以做隐式类型转换的语言,叫做弱类型,不允许隐式类型转换的语言,叫做强类型。
动态类型只适合简单场景,对于大型项目(尤其是多人合作的大型项目)却不太合适,因为动态类型没法做约束,代码中会隐藏大量的隐患。而静态类型可以保证类型安全,很好的保证代码的健壮性,减少bug。
1.1 类型系统分类
静态语言都有自己的类型系统,从简单到复杂可以分为3中类型:
- 简单类型系统
简单类型系统可以支持定义number,boolean,string,以及class。同时编译器也保证编译阶段的类型检测,保证类型安全。那么这样的类型系统有什么缺点呢?就是太死板,比如我们定义一个支持float和int的函数,需要用多态实现:
int add(int a, int b) {
return a + b;
}
int add(float a, float b) {
return a + b;
}
那么这样的缺点如何解决呢,我们很容想到,如果类型不是在定义的时候确定,而是在调用的时候确定就好了,这就是第二种类型系统——支持泛型的类型系统。
-
支持泛型的类型系统
支持泛型的类型系统:泛型的英文是 Generic Type,通用的类型,它可以代表任何一种类型,也叫做类型参数。这样的类型系统大大增加了语言的灵活性,比如上面的例子可以如下改写:
T add<T>(T a, T b){ return a + b; }泛型系统的类型不是在定义的时候确定的,而是在调用时候确定,同时保证类型可以被记一下来,java就是这样的类型系统。支持泛型的类型系统极大的增强了语言的灵活性。但是对于JavaScript来说,还是远远不够的,因为JavaScript太过灵活了。
为了满足JavaScript灵活性,就要有第三种类型系统——支持编程的类型系统。
-
支持编程的类型系统(图灵完备)
对传入的参数进行各种逻辑运算,最终产生新的类型,这就是可编程的类型系统。
JavaScript真的需要这么复杂的类型系统吗?答案是确定。因为JavaScript实在是太灵活了,对于Java来说,所有的对象都是new出来的,而JavaScript对象可以是new出来,也可以是字面量,这就需要复杂的类型系统来保证类型的完备,比如下面例子,在Java中是绝对不能实现:
function<T>(obj: T, key: extends keyof T):T[key] {
return T[key];
}
TypeScript 的类型系统是图灵完备的,也就是能描述各种可计算逻辑。简单点来理解就是循环、条件等各种 JS 里面有的语法它都有,JS 能写的逻辑它都能写。
那么下面就让我们开始TypeScript类型之旅吧。
2. 基础类型
这一部分的类型也是 JavaScript 中的基本类型,但也增加了一些非常有用的类型如tuple和 enum,虽然他们的本质是Array和object。
Boolean
boolean是很简单false和true 的集合
let isCompleted: boolean = false;
Number
number是所有数字的集合,各个进制数字的写法也和 JavaScript 保持一致。
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
String
在 TypeScript 中同样支持双引号和单引号的字符串。
let color: string = "blue";
color = 'red';
Array
TypeScript 中数组的写法有两种。
// Type[]
let list: number[] = [1, 2, 3];
或
// Array<Type>
let list: Array<number> = [1, 2, 3];
而因为有可能用到 JSX 语法,第二种写法会和 JSX 语法冲突,为了保持一致性,所以推荐使用第一种。
数组类型也是一些变量的集合,例如:
number[]就是[number]、[number, number]、[number, number, number]...的集合,而其中的number则如上所说是所有数字的集合。
Tuple
元组类型是几种类型的数组形式的固定组合,如下:
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error
元组类型在我看来是数组类型的子类型,如上number[]就是一些元组的集合。
而[string, number]则是string | number[]的子类型。即string | number[]为 number[]、string[]、[string, number ...]...,其中就包括一个[string, number]。
Enum
enum这个操作符比较特殊,它的定位类似于var、let、const,用于声明变量,但它只支持特定结构的变量声明,而这个结构就是一个对象。它的用法如下:
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
它的本质就是构建了一个对象,对象中属性的值默认从0开始,依次加1。 当然你也可以设置为其他的值。
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
Object
object是对象类型,即它是所有对象的集合。
2.1 特殊类型
这里会介绍一下在 TypeScript 类型系统中的空值、bottom type、top type等。
Null & Undefined
null和undefined都是空值,但因为在 JavaScript 中typeof操作符的行为,未定义的变量和定义了但未赋值的变量都是undefined,所以推荐定义了变量之后不会即刻赋值时,设置空值为null,这样可以区分开typeof的行为,当然这样可能会引入另一个问题,即typeof null为object但依旧推荐这样做,可以在判断空值时直接使用foo != null。
Unknown
unknown是 TypeScript 中的 top type,即任何类型都是它的子类型,它是 TypeScript 中所有可选值的集合。
Never
never是 TypeScript 中的 bottom type,即它是任何类型的子类型,但在 TypeScript 中它有着其他的作用,比如,当尝试给一个never类型的变量赋值时,中断当前程序运行,并抛出异常。
Any
any是 TypeScript 中非常特殊的类型,它既是 top type,又是 bottom type,即任何类型都是它的子类型,它又是任何类型的子类型。是不是很矛盾?但它的价值就在这里,TypeScript 目前还无法完美支持 JavaScript 的所有能力,any就相当于一个缓冲,就是当你要做的事情 TypeScript 当前的类型系统还不支持的时候,就用any告诉编译器,这个你还不懂,但它是对的,然后编译器会非常相信你,当遇到any的时候,不做任何的类型检查。
所以在使用any之前,你要用尽浑身解数,尝试用 TypeScript 当前支持的能力来完成你所要做的工作,但当你发现 TypeScript 无法做到的时候,你就可以使用any了。
3 高级类型
3.1 操作符
在进入高级类型之前,我们先看几个操作符:
- typeof
typeof在typescript中还可以用来返回一个变量的声明类型,如果不存在,则获取该类型的推论类型。
let n: number = 1
type TN = typeof n
/** 等同于
* TN == number
*/
let s:string = 'hello world'
type TS = typeof s
/** 等同于
* TS == string
*/
let a: Array<number> = []
type TA = typeof a
/** 等同于
* TA == number[]
*/
let sy: Symbol = Symbol()
type TSY = typeof sy
/** 等同于
* TSY == Symbol
*/
let obj = {name:'zhangsan', age:28, male:true, run:()=>'run'}
type TO = typeof obj
/** 等同于
* type TO = {
* name: string;
* age: number;
* male: boolean;
* run: () => string;
* }
*/
- keyof
TypeScript允许我们遍历某种类型的属性,并通过keyof操作符提取其属性的名称,类似Object.keys方法。keyof操作符是在TypeScript 2.1版本引入的,可以用于获取某种类型的所有键,其返回类型是联合类型
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string
keyof可以结合typeof一起使用,用于获取变量声明类型的key值的联合类型。这也给了我们一种获取联合类型的方式
let colors = {
red: 'Red',
green:'Green',
blue:'Blue'
}
type TColors = keyof typeof colors // 'red' | 'green' | 'blue'
- in
用于遍历类型的属性key值,一般和keyof联合使用:
interface Person {
name: string;
age: number;
}
type Partial<T> {
[key in keyof T]?: T[key]
}
type Per = Partial<Person>;
/**
得到如下类型
type per = {
name?: string;
age?: number;
}
*/
- extends
这里的extends只是在类型系统的中的使用方法,不包括class的继承。
extends在Typescript用法较多,我们一一剖析:
- 用于限制类型范围:
function get<T extends Object, Key extends keyof T>(obj: T, key: Key): T[Key] {
return obj[key];
}
- 用于条件类型
typescript 2.8引入了条件类型表达式,类似于三元运算符,在这种意义下,extends提供了判断语句:
type isNull<T> = T extends null | undefined ? true : false;
type notNull = isNull<number>; // ==> false
type isNull = isNull<null>; // ==> true
- infer
infer可以用来声明一个待推断的类型变量,简单来说,相当于JavaScript中的const。
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
type T0 = ReturnType<() => string> // string
type T1 = ReturnType<(s: string) => void> // void
type T2 = ReturnType<<T>() => T> // unkonwn
下面让我们根据这些操作符来玩转Typescript的高级类型。
3.2 模式匹配
模式匹配是我们使用Typescript最有用的特性之一,我们要实现我们要实现数组的增删改查,数据的提取,字符串的操作等,都需要用到这个特性。而Typescript 实现模糊匹配的操作符主要是infer,用于声明局部变量。
infer操作符只能和extends配合使用
关于条件类型中
infer的官方文档:Inferring Within Conditional Types
那么这个属性怎么用呢,我们从针对字符串,数组,函数等操作来玩这个操作符。
3.2.1 数组
- First
首先我们从数组中抽取第一个元素
type Arr = [1, 2, 3];
type FirstArr<T extends unknown[]> = T extends [infer First, ...unknown[]] ? First : never;
对数组进行模糊匹配,我们要抽出第一个元素类型,放到通过infer声明的局部变量中,后面的可以是任意类型,放到unknown[]中,然后把局部变量First返回。
- End
同理我们可以抽取最后一个元素
type Arr = [1, 2, 3];
type EndArr<T extends unknown[]> = T extends [...unknown[], infer End] ? End : never;
- Rest
既然能取首位,我们就能取剩余类型
type Arr = [1, 2, 3];
type RestArr<T extends unknown[]> = T extends [infer First, ...infer Rest, infer End] ? Rest : never;
type End = RestArr<Arr>; // [2]
3.2.2 字符串
字符串类型也同样可以做模式匹配,匹配一个模式字符串,把需要提取的部分放到 infer 声明的局部变量里。
- startWith
我们判断一个字符是否以某个字符开头:
type StartWith<T extends string, Prefix extends string> =
T extends `${Prefix}${string}` ? true : false;
type IsStart = StartWith<'hello word', 'hello'>; // true
- trim
字符串可以做模糊匹配,当然也可以做trim
type Trim<T extends string> =
T extends `${' '|'\n'|'\t'}${infer Rest}${' '|'\n'|'\t'}` ? Rest : never;
type NoTrim = Trim<' hello word '>; // hello word
- Replace
我们可以对字符进行trim操作,那我们就肯定可以做replace操作
type Replace<T extends string, From extends srting, To extends> =
T extends `${infer Prefix}${From}${infer Suffix}`
? `${infer Prefix}${To}${infer Suffix}`
: never
3.2.2 函数
当然我们也可以对函数进行模式匹配,比如提取参数,返回值类型
- Parameters
获取函数的参数类型:
type Parameters<T extends (...arg: any[]) => any> =
T extends (...args: infer Args) => any ? Args : never;
type TestParameter = Parameters<(name: string, age: number) => void>; //[name: string, age: number]
- ReturnType
能获取参数,就能获取返回值
type ReturnType<T extends (...arg: any[]) => any> =
T extends (...args: any[]) => infer Return ? Return : never;
type TestReturnType = ReturnType1<(name: string, age: number) => number>; // number
3.3 重新构造
typescript 支持三种可以声明任意类型的变量,type,infer,类型参数,但三种方式都不能对原始类型进行修改,如果我们想修改原始类型,就需要重新构造,产生新的类型。
3.3.1 数组构造
针对数组我们可以做到增删改。
- Push
type ArrayPush<T extends unknown[], Ele extends unknown> = [...T, Ele]
type TestPush<[1,2], 3>; // [1, 2, 3]
- Unshift
可以在后面添加,当然也可以在前面添加
type ArrayUnshift<T extends unknown[], Ele extends unknown> = [Ele, ...T]
type TestUnshift<[1,2], 3>; // [3, 1, 2]
3.3.2 字符型
我们可以对数组进行增删改,那么我们可以对字符串有哪些操作呢?
- Uppercase
首先,我们可以对字符串进行首字母大写的操作
type GetUppercase<Str extends string> =
Str extends `${infer Prefix}${infer Rest}`
? `${Uppercase<Prefix>}${Rest}` : never;
- CamelCase
既然我们可以转写首字母,当然我们也可以将下划线类型,转为驼峰
type CamelCase<Str extends string> =
Str extends `${infer Left}_${infer Right}` ?
`${Left}${GetUppercase<Right>}` : never;
type TestCamelCase = CamelCase<'test_came'>; // testCame
上面的例子只是转换了一个下划线,如果有多个下划线我们怎么修改呢,答案是递归调用,后面我们在玩递归时会再次做这样的转换。
- DeleteStr
既然能做转换,我们肯定就能做删除
type DeleteStr<Str extends string, Target extends string> =
Str extends `${infer Prefix}${Target}${infer Subfix}`
? `${Prefix}${Subfix}` : never;
type TestDelete = DeleteStr<'hello world', ' world'>; // hello
3.3.3 函数操作
针对函数操作,我们可以修改返回值,添加删除参数,修改参数类型等。
- AddParameters
首先我们针对函数添加参数类型:
type AddParameters<Fun extends (...args: any[]) => any, Arg> =
Fun extends (...args: infer Args) => infer Return
? (...args: [...Args, Arg]) => Return : never;
- ChangeReturnType
我们还可以修改返回值类型
type ChangeReturnType<Fun extends (...args: any[]) => any, TeturnType> =
Fun extends (...args: infer Args) => infer Return
? (...args: Args) => TeturnType : never;
4 递归判断
我们都知道一门语言必不可少的功能就是循环判断,只要有完备的循环判断,就可以构建出复杂的程序出来,那么做为图灵完备类型的Typescript类型系统,必定也会提供这两个功能:
- extends:可以做为判断条件
- 递归:递归模拟循环
前面我们已经用到很多以extends来模拟判断的语句,比如:
type AddParameters<Fun extends (...args: any[]) => any, Arg> =
Fun extends (...args: infer Args) => infer Return
? (...args: [...Args, Arg]) => Return : never;
下面我们主要来玩递归,看看用递归我们能实现那些骚操作。
4.1 递归复用
递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。递归一词还较常用于描述以自相似方法重复事物的过程。例如,当两面镜子相互之间近似平行时,镜中嵌套的图像是以无限递归的形式出现的。也可以理解为自我复制的过程。
上面是维基百科对递归的解释
4.1.1 数组递归
- Includes
判断数组是否包含某一项是在JS最常用的功能,在Typescript系统中也是可以实现的。
type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
type ArrayIncludes<Arr extends unknown[], Ele extends unknown> =
Arr extends [infer First, ...infer Rest] ?
IsEqual<First, Ele> extends true
? true
: ArrayIncludes<Rest, Ele>
: false;
type IsArrayIncludes = ArrayIncludes<[1,2,3,4], 1> // true
- Unique
我们能判断是否包含,就能做到去重操作
type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
type ArrayUnique<Arr extends unknown[], Res extends unknown[] = []> =
Arr extends [infer First, ...infer Rest] ?
ArrayIncludes<Res, First> extends false ?
ArrayUnique<Rest, [ ...Res, First]>
: ArrayUnique<Rest, Res>
: Res;
type TestArrayUnique = ArrayUnique<[2,3,2,1]>; // [2,3,1]
- ArrayCreate
因为在Typescrip类型系统中中是没有new的,所以我们想要构造一个数组就需要用到递归逐个添加元素:
type ArrayCreate<Len extends number, Ele extends unknown = unknown, Res extends unknown[] = []> =
Res['length'] extends Len ?
Res
: ArrayCreate<Len, Ele, [...Res, Ele]>;
type TestArrayCreate = ArrayCreate<5, 10>; // [10, 10, 10, 10, 10]
4.1.2 字符串的递归操作
- CamelCaseAll
在前面我们完重新构造时写过一个下划线转驼峰的类型,但是当时我们转了一层,hello_world可以转换为helloWorld,但是对于想is_need_update转换时就不能支持,因为最终转换出来为isNeed_update,如果想完成这样的转换,需要做递归操作
type CamelCaseAll<Str extends string> =
Str extends `${infer Left}_${infer Right}${infer Rest}` ?
`${Left}${GetUppercase<Right>}${CamelCase<Rest>}` : Str;
type TestCamelCase = CamelCaseAll<'is_need_update'>; // isNeedUpdate
- ReplaceAll
我们前面写过Replace,下面写一个增强版
type ReplaceAll<Str extends string, From extends string, To extends string> =
Str extends `${infer Prefix}${From}${Subfix}` ?
`${Prefix}${To}${ReplaceAll<Subfix, From, To>}`
: Str;
type TestReplaceAll = ReplaceAll<'hello world world', 'world', 'hello'>; // 'hello hello hello'
4.1.3 对象递归
我们知道Typescript我们提供了一个高级类型Readonly,就是将所有的属性变为Readonly,我们来试试实现这个功能
interface Person {
name: string;
age: number
}
type MyReadonly<T extends object> = {
readonly [P in keyof T]: T[p]
}
上面我们已经实现了,但是如果我们对象有嵌套:
interface Person {
name: string;
age: number;
addresses: {
address: string
}
}
此时,只能对外层进行修改,但是不能对内层进行修改
interface Person {
readonly name: string;
readonly age: number;
readonly addresses: {
address: string
}
}
如果想做到深层修改,就需要对对象进行递归
type DeepReadonly<T extends object> =
T extends any ?
{
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}
: never
外面为什么要extends any,是为了触发执行,因为在typescript中是在访问时才触发更新。
5. 加减乘除
我们接下来玩typescript类型中最好玩的一部分,就是加减乘除,当然在typescript的类型系统中是没有办法做加减乘除的,那我们怎么实现呢,前面我们已经用了数组的length属性属性了
type A = [1,2,3]['length']; // 这样我们就可以做操作
那我们就可以用数组的length模拟加减乘除了。
- 加
首先我们看看加法怎么实现,前面我们实现过一个类型:ArrayCreate,就是用来创建数组,那么加法就好实现
type Add<A extends number, B extends number> = [...ArrayCreate<A>, ...ArrayCreate<B>]['length'];
- 减法
减法的运算为:差 = 被减数 - 减数,转换为数组则为:差值部分 = 整体部分 - 减去部分,即 整体部分 = 减去部分 + 差值部分(被减数 = 减数 + 差)
type Subtract<A extends number, B extends number> = ArrayCreate<A> extends [...arr1: ArrayCreate<B>, ...arr2: infer Rest] ? Rest['length'] : never
- 乘法
乘法的运算为:m + n,转换为数组思路为 n 个长度为 m 的数组相接的新数组的长度。
type Multiply<A extends number, B extends number, Pre extends unknown[] = []> = B extends 0 ? Pre['length'] : Multiply<A, Subtract<B, 1>, [...ArrayCreate<A, number>, ...Pre]>;
- 除法
除法的运算为:m / n,转换为数组思路为长度为 m 的数组由多少个长度为 n 的数组组成。
type Divide<A extends number, B extends number, Pre extends unknown[] = []> = A extends 0 ? Pre['length'] : Divide<Subtract<A, B>, B, [unknown, ...Pre]>;