欢迎各位多加指正
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
,用于声明变量,但它只支持特定结构的变量声明,而这个结构就是一个对象。它的用法如下:
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }0
它的本质就是构建了一个对象,对象中属性的值默认从0
开始,依次加1
。 当然你也可以设置为其他的值。
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }1
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中还可以用来返回一个变量的声明类型,如果不存在,则获取该类型的推论类型。
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }2
keyof
TypeScript允许我们遍历某种类型的属性,并通过keyof操作符提取其属性的名称,类似Object.keys方法。keyof操作符是在TypeScript 2.1版本引入的,可以用于获取某种类型的所有键,其返回类型是联合类型
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }3
keyof可以结合typeof一起使用,用于获取变量声明类型的key值的联合类型。这也给了我们一种获取联合类型的方式
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }4
in
用于遍历类型的属性key值,一般和keyof联合使用:
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }5
extends
这里的extends只是在类型系统的中的使用方法,不包括class的继承。
extends在Typescript用法较多,我们一一剖析:
用于现在类型范围:
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }6
用于条件类型
typescript 2.8引入了条件类型表达式,类似于三元运算符,在这种意义下,extends提供了判断语句:
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }7
infer
infer可以用来声明一个待推断的类型变量,简单来说,相当于JavaScript中的const。
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }8
下面让我们根据这些操作符来玩转Typescript的高级类型。
3.2 模式匹配
模式匹配是我们使用Typescript最有用的特性之一,我们要实现我们要实现数组的增删改查,数据的提取,字符串的操作等,都需要用到这个特性。而Typescript 实现模糊匹配的操作符主要是infer,用于声明局部变量。
infer操作符只能和extends配合使用
关于条件类型中 infer
的官方文档:Inferring Within Conditional Types
那么这个属性怎么用呢,我们从针对字符串,数组,函数等操作来玩这个操作符。
3.2.1 数组
First
首先我们从数组中抽取第一个元素
int add(int a, int b) { return a + b; } int add(float a, float b) { return a + b; }9
对数组进行模糊匹配,我们要抽出第一个元素类型,放到通过infer声明的局部变量中,后面的可以是任意类型,放到unknown[]中,然后把局部变量First返回。
End
同理我们可以抽取最后一个元素
T add<T>(T a, T b){ return a + b; }0
Rest
既然能取首位,我们就能取剩余类型
T add<T>(T a, T b){ return a + b; }1
3.2.2 字符串
字符串类型也同样可以做模式匹配,匹配一个模式字符串,把需要提取的部分放到 infer 声明的局部变量里。
startWith
我们判断一个字符是否以某个字符开头:
T add<T>(T a, T b){ return a + b; }2
trim
字符串可以做模糊匹配,当然也可以做trim
T add<T>(T a, T b){ return a + b; }3
Replace
我们可以对字符进行trim操作,那我们就肯定可以做replace操作
T add<T>(T a, T b){ return a + b; }4
3.2.2 函数
当然我们也可以对函数进行模式匹配,比如提取参数,返回值类型
Parameters
获取函数的参数类型:
T add<T>(T a, T b){ return a + b; }5
ReturnType
能获取参数,就能获取返回值
T add<T>(T a, T b){ return a + b; }6
3.3 重新构造
typescript 支持三种可以声明任意类型的变量,type,infer,类型参数,但三种方式都不能对原始类型进行修改,如果我们想修改原始类型,就需要重新构造,产生新的类型。
3.3.1 数组构造
针对数组我们可以做到增删改。
Push
T add<T>(T a, T b){ return a + b; }7
Unshift
可以在后面添加,当然也可以在前面添加
T add<T>(T a, T b){ return a + b; }8
3.3.2 字符型
我们可以对数组进行增删改,那么我们可以对字符串有哪些操作呢?
Uppercase
首先,我们可以对字符串进行首字母大写的操作
T add<T>(T a, T b){ return a + b; }9
CamelCase
既然我们可以转写首字母,当然我们也可以将下划线类型,转为驼峰
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }0
上面的例子只是转换了一个下划线,如果有多个下划线我们怎么修改呢,答案是递归调用,后面我们在玩递归时会再次做这样的转换。
DeleteStr
既然能做转换,我们肯定就能做删除
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }1
3.3.3 函数操作
针对函数操作,我们可以修改返回值,添加删除参数,修改参数类型等。
AddParameters
首先我们针对函数添加参数类型:
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }2
ChangeReturnType
我们还可以修改返回值类型
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }3
4 递归判断
我们都知道一门语言必不可少的功能就是循环判断,只要有完备的循环判断,就可以构建出复杂的程序出来,那么做为图灵完备类型的Typescript类型系统,必定也会提供这两个功能:
extends:可以做为判断条件
递归:递归模拟循环
前面我们已经用到很多以extends来模拟判断的语句,比如:
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }2
下面我们主要来玩递归,看看用递归我们能实现那些骚操作。
4.1 递归复用
递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。递归一词还较常用于描述以自相似方法重复事物的过程。例如,当两面镜子相互之间近似平行时,镜中嵌套的图像是以无限递归的形式出现的。也可以理解为自我复制的过程。
上面是维基百科对递归的解释
4.1.1 数组递归
Includes
判断数组是否包含某一项是在JS最常用的功能,在Typescript系统中也是可以实现的。
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }5
Unique
我们能判断是否包含,就能做到去重操作
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }6
ArrayCreate
因为在Typescrip类型系统中中是没有new的,所以我们想要构造一个数组就需要用到递归逐个添加元素:
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }7
4.1.2 字符串的递归操作
CamelCaseAll
在前面我们完重新构造时写过一个下划线转驼峰的类型,但是当时我们转了一层,hello_world可以转换为helloWorld,但是对于想is_need_update转换时就不能支持,因为最终转换出来为isNeed_update,如果想完成这样的转换,需要做递归操作
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }8
ReplaceAll
我们前面写过Replace,下面写一个增强版
function<T>(obj: T, key: extends keyof T):T[key] { return T[key]; }9
4.1.3 对象递归
我们知道Typescript我们提供了一个高级类型Readonly,就是将所有的属性变为Readonly,我们来试试实现这个功能
let isCompleted: boolean = false;0
上面我们已经实现了,但是如果我们对象有嵌套:
let isCompleted: boolean = false;1
此时,只能对外层进行修改,但是不能对内层进行修改
let isCompleted: boolean = false;2
如果想做到深层修改,就需要对对象进行递归
let isCompleted: boolean = false;3
外面为什么要extends any,是为了触发执行,因为在typescript中是在访问时才触发更新。
5. 加减乘除
我们接下来玩typescript类型中最好玩的一部分,就是加减乘除,当然在typescript的类型系统中是没有办法做加减乘除的,那我们怎么实现呢,前面我们已经用了数组的length属性属性了
let isCompleted: boolean = false;4
那我们就可以用数组的length模拟加减乘除了。
加
首先我们看看加法怎么实现,前面我们实现过一个类型:ArrayCreate,就是用来创建数组,那么加法就好实现
let isCompleted: boolean = false;5
减法
减法的运算为:差 = 被减数 - 减数,转换为数组则为:差值部分 = 整体部分 - 减去部分,即 整体部分 = 减去部分 + 差值部分(被减数 = 减数 + 差)
let isCompleted: boolean = false;6
乘法
乘法的运算为:m + n,转换为数组思路为 n 个长度为 m 的数组相接的新数组的长度。
let isCompleted: boolean = false;7
除法
除法的运算为:m / n,转换为数组思路为长度为 m 的数组由多少个长度为 n 的数组组成。
let isCompleted: boolean = false;8
文件参考
原文:https://juejin.cn/post/7099100459213783077