TS 入门学习
什么是 TypeScript
TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
简而言之,TypeScript是JavaScript的超集,具有可选的类型并可以编译为纯JavaScript。从技术上讲TypeScript就是具有静态类型的 JavaScript 。
TypeScript优缺点
优点
- 增强代码的可维护性,尤其在大型项目的时候效果显著
- 友好地在编辑器里提示错误,编译阶段就能检查类型发现大部分错误
- 支持最新的JavaScript新特特性
- 周边生态繁荣,vue3已全面支持 typescript
缺点
- 需要一定的学习成本
- 和一些插件库的兼容并不是特别完美,如以前在 vue2 项目里使用 typescript就并不是那么顺畅
- 增加前期开发的成本,毕竟你需要写更多的代码(但是便于后期的维护)
安装环境
安装typescript
首先,我们可以新建一个空文件夹,用来学习 ts,例如我在文件夹下新建了个 helloworld.ts
1 | npm install -g typescript // 全局安装 ts |
不记得自己是否已经安装过 typescript 的,可以使用以下命令来验证:
1 | tsc -v |
如果出现版本,则说明已经安装成功
1 | Version 4.6.3 |
生成 tsconfig.json 配置文件
1 | tsc --init |
执行命令后我们就可以看到生成了一个 tsconfig.json 文件,里面有一些配置信息,我们暂时先按下不表
在我们helloworld.ts
文件中,随便写点什么
1 | const s:string = "彼时彼刻,恰如此时此刻"; |
控制台执行 tsc helloworld.ts
命令,目录下生成了一个同名的 helloworld.js 文件,代码如下
1 | var s = "彼时彼刻,恰如此时此刻"; |
通过tsc命令,发现我们的typescript代码被转换成了熟悉的js代码
我们接着执行
1 | node helloworld.js |
即可看到输出结果
安装 ts-node
那么通过我们上面的一通操作,我们知道了运行tsc命令就可以编译生成一个js文件,但是如果每次改动我们都要手动去执行编译,然后再通过 node命令才能查看运行结果岂不是太麻烦了。
而 ts-node 正是来解决这个问题的
1 | npm i -g ts-node // 全局安装ts-node |
有了这个插件,我们就可以直接运行.ts文件了
我们试一下
1 | ts-node helloworld.ts |
可以看到我们的打印结果已经输出
后续我们的示例都可以通过这个命令来进行验证
接下来我们就可以正式进入到 typescript 的学习之旅了
TypeScript 基础类型
Boolean 类型
1 | const flag: boolean = true; |
Number 类型
1 | const count: number = 10; |
String 类型
1 | let name: string = "树哥"; |
Enum 类型
枚举类型用于定义数值集合,使用枚举我们可以定义一些带名字的常量。使用枚举可以清晰地表达意图或创建一组有区别的用例。,如周一到周日,方位上下左右等
- 普通枚举
初始值默认为 0 其余的成员会会按顺序自动增长 可以理解为数组下标
1 | enum Color { |
- 设置初始值
1 | enum Color { |
- 字符串枚举
1 | enum Color { |
- 常量枚举
使用 const 关键字修饰的枚举,常量枚举与普通枚举的区别是,整个枚举会在编译阶段被删除 我们可以看下编译之后的效果
1 | const enum Color { |
Array 类型
对数组类型的定义有两种方式:
1 | const arr: number[] = [1,2,3]; |
元组(tuple)类型
上面数组类型的方式,只能定义出内部全为同种类型的数组。对于内部不同类型的数组可以使用元组类型来定义
元组( Tuple )表示一个已知数量和类型的数组,可以理解为他是一种特殊的数组
1 | const tuple: [number, string] = [1, "zhangmazi"]; |
需要注意的是,元组类型只能表示一个已知元素数量和类型的数组,长度已指定,越界访问会提示错误。例如,一个数组中可能有多种类型,数量和类型都不确定,那就直接any[]。
undefined和null
默认情况下 null 和 undefined 是所有类型的子类型。也就是说你可以把 null 和 undefined 赋值给其他类型。
1 | let a: undefined = undefined; |
如果你在tsconfig.json指定了”strictNullChecks”:true ,即开启严格模式后, null 和 undefined 只能赋值给 void 和它们各自的类型。
1 | // 启用 --strictNullChecks |
any 类型
any会跳过类型检查器对值的检查,任何值都可以赋值给any类型
1 | let value: any = 1; |
void 类型
void 意思就是无效的, 一般只用在函数上,告诉别人这个函数没有返回值。
1 | function sayHello(): void { |
never 类型
never 类型表示的是那些永不存在的值的类型。例如never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型
值会永不存在的两种情况:
- 1 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值(因为抛出异常会直接中断程序运行,这使得程序运行不到返回值那一步,即具有不可达的终点,也就永不存在返回了)
- 2 函数中执行无限循环的代码(死循环),使得程序永远无法运行到函数返回值那一步,永不存在返回。
1 | // 异常 |
Unknown 类型
unknown与any一样,所有类型都可以分配给unknown:
1 | let value: unknown = 1; |
unknown与any的最大区别是:
任何类型的值可以赋值给any,同时any类型的值也可以赋值给任何类型。unknown 任何类型的值都可以赋值给它,但它只能赋值给unknown和any
对象类型
这里所说的对象类型,就是我们常说的函数、{}、数组、类
object, Object 和 {} 类型
- object object 类型用于表示所有的非原始类型,即我们不能把 number、string、boolean、symbol等 原始类型赋值给 object。在严格模式下,null 和 undefined 类型也不能赋给 object。
1 | let object: object; |
- Object
大 Object 代表所有拥有 toString、hasOwnProperty 方法的类型 所以所有原始类型、非原始类型都可以赋给 Object(严格模式下 null 和 undefined 不可以)
1 | let bigObject: Object; |
- {}
{} 空对象类型和大 Object 一样 也是表示原始类型和非原始类型的集合
类
在 TypeScript 中,我们通过 Class 关键字来定义一个类
1 | class Person { |
数组
1 | const flag1: number[] = [1, 2, 3]; |
函数
函数声明
1 | function add(x: number, y: number): number { |
函数表达式
1 | const add = function(x: number, y: number): number { |
接口定义函数
1 | interface Add { |
可选参数
1 | function add(x: number, y?: number): number { |
默认参数
1 | function add(x: number, y: number = 0): number { |
剩余参数
1 | function add(...numbers: number[]): number { |
函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。
1 | function add(x: number, y: number): number; |
上面示例中,我们给同一个函数提供多个函数类型定义,从而实现函数的重载
需要注意的是:
函数重载真正执行的是同名函数最后定义的函数体 在最后一个函数体定义之前全都属于函数类型定义 不能写具体的函数实现方法 只能定义类型
具体示例原理可参考[1]
类型推论
如果没有明确的指定类型,那么 TypeScript 会依照类型推论的规则推断出一个类型。
1 | let x = 1; |
上面的代码等价于
1 | let x: number = 1; |
通过上述示例我们可以看出,我们没有给 x 指定明确类型的时候,typescript 会推断出 x 的类型是 number。
而如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:
1 | let x; |
类型断言
某些情况下,我们可能比typescript更加清楚的知道某个变量的类型,所以我们可能希望手动指定一个值的类型
类型断言有两种方式
- 尖括号写法
1 | let str: any = "to be or not to be"; |
- as 写法
1 | let str: any = "to be or not to be"; |
非空断言
在上下文中当类型检查器无法断定类型时,可以使用缀表达式操作符 !
进行断言操作对象是非 null 和非 undefined 的类型,即x!的值不会为 null 或 undefined
1 | let user: string | null | undefined; |
确定赋值断言
1 | let value:number |
我们定义了变量, 没有赋值就使用,则会报错
通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。
1 | let value!:number |
联合类型
联合类型用|
分隔,表示取值可以为多种类型中的一种
1 | let status:string|number |
类型别名
类型别名用来给一个类型起个新名字。它只是起了一个新名字,并没有创建新类型。类型别名常用于联合类型。
1 | type count = number | number[]; |
交叉类型
交叉类型就是跟联合类型相反,用&
操作符表示,交叉类型就是两个类型必须存在
1 | interface IpersonA{ |
person 即是 IpersonA 类型,又是 IpersonB 类型
注意:交叉类型取的多个类型的并集,但是如果key相同但是类型不同,则该key为never类型
1 | interface IpersonA { |
类型守卫
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。
换句话说:类型守卫是运行时检查,确保一个值在所要类型的范围内
目前主要有四种的方式来实现类型保护:
- 1、in 关键字
1 | interface InObj1 { |
- 2、typeof 关键字
1 | function isTypeof( val: string | number) { |
typeof 只支持:typeof ‘x’ === ‘typeName’ 和 typeof ‘x’ !== ‘typeName’,x 必须是 ‘number’, ‘string’, ‘boolean’, ‘symbol’。
- 3、instanceof
1 | function creatDate(date: Date | string){ |
- 4、自定义类型保护的类型谓词
1 | function isNumber(num: any): num is number { |
接口
我们使用接口来定义对象的类型。接口是对象的状态(属性)和行为(方法)的抽象(描述)
简单理解就是:为我们的代码提供一种约定
我们使用关键字interface来声明接口
1 | interface Person { |
我们定义了一个接口 Person,接着定义了一个变量 tom,它的类型是 Person。这样,我们就约束了 tom 的形状必须和接口 Person 一致。
接口一般首字母大写。(当然挺多人也习惯 I 大写字母开头,用来表示这是一个接口)
设置接口可选|只读
1 | interface Person { |
- 可选属性,我们最常见的使用情况是,不确定这个参数是否会传,或者存在。
- 只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray 类型,它与 Array 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。
索引签名
有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
1 | interface Person { |
我们规定 以 string 类型的值来索引,索引到的是一个 any 类型的值
接口与类型别名的区别
实际上,在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。
TypeScript 的核心原则之一是对值所具有的结构进行类型检查。而接口的作用就是为这些类型命名和为你的代码或第三方代码定义数据模型。
type(类型别名)会给一个类型起个新名字。type 有时和 interface 很像,但是可以作用于原始值(基本类型),联合类型,元组以及其它任何你需要手写的类型。起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。给基本类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
接口和类型别名都可以用来描述对象或函数的类型,只是语法不同
1 | type MyTYpe = { |
都允许扩展
- interface 用
extends
来实现扩展
1 | interface MyInterface { |
- type 使用
&
实现扩展
1 | type MyType = { |
不同点
- type可以声明基本数据类型别名/联合类型/元组等,而interface不行
1 | // 基本类型别名 |
- interface能够合并声明,而type不行
1 | interface Person { |
泛型
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
举个例子,比如我们现在有个这样的需求,我们要实现一个这样的函数,函数的参数可以是任何值,返回值就是将参数原样返回,并且参数的类型是 string,函数返回类型就为 string?
你很容易写下:
1 | function getValue(arg:string):string { |
现在需求有变,需要返回一个 number 类型的值,你会说,联合类型就完事了:
1 | function getValue(arg:string | number):string | number { |
但是这样又有一个问题,就是如果我们需要返回一个 boolean 类型,string 数组甚至任意类型呢,难道有多少个就写多少个联合类型?
是的,我们直接用 any 就行了!
1 | function getValue(arg:any):any { |
尽管 any 大法好,很多时候 any 也确实能够解决不少问题,但是这样也不符合我们的需求了,传入和返回都是 any 类型,传入和返回并没有统一
作为一个骚有最求的程序员,我们还能不能有其他解决办法呢?
这个时候就要祭出我们的泛型了
基本使用
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
上面的需求,我们如果用泛型来解决的话:
1 | function getValue<T>(arg:T):T { |
泛型的语法是尖括号 <>
里面写类型参数,一般用 T
来表示第一个类型变量名称,其实它可以用任何有效名称来代替,比如我们用NIUBI
也是编译正常的
泛型就像一个占位符一个变量,在使用的时候我们可以将定义好的类型像参数一样传入,原封不动的输出
使用
我们有两种方式来使用:
-
- 定义要使用的类型,比如:
1 | getValue<string>('树哥'); // 定义 T 为 string 类型 |
-
- 利用 typescript 的类型推断,比如:
1 | getValue('树哥') // 自动推导类型为 string |
多个参数
其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U
1 | function getValue<T, U>(arg:[T,U]):[T,U] { |
typescript 给我们自动推断出输入、返回的类型
泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
1 | function getLength<T>(arg:T):T { |
因为泛型 T 不一定包含属性 length,那么我想 getLength 这个函数只允许传入包含 length 属性的变量,该怎么做呢
这时,我们可以使用extends
关键字来对泛型进行约束
1 | interface Lengthwise { |
使用:
1 | const str = getLength('树哥') |
这里可以看出,不管你是 str,arr 还是obj,只要具有 length 属性,都可以
具体参考轻松拿下 TS 泛型[2]
泛型接口
在定义接口的时候指定泛型
1 | interface KeyValue<T,U> { |
泛型类
1 | class Test<T> { |
泛型类型别名
1 | type Cart<T> = { list: T[] } | T[]; |
泛型参数的默认类型
我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。有点 js 里函数默认参数的意思。
1 | function createArray<T = string>(length: number, value: T): Array<T> { |
泛型工具类型
- typeof
关键词除了做类型保护,还可以从实现推出类型,
1 | //先定义变量,再定义类型 |
- keyof
可以用来获取一个对象接口中的所有 key 值
1 | interface Person { |
- in
用来遍历枚举类型:
1 | type Keys = "a" | "b" | "c" |
- infer
在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。
1 | type ReturnType<T> = T extends ( |
infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。
- extends
有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。
1 | interface Lengthwise { |
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
1 | loggingIdentity(3); // Error, number doesn't have a .length property |
当我们传入合法的类型的值,即包含 length 属性的值时:
1 | loggingIdentity({length: 10, name: '张麻子'}); // 编译正确 |
- 索引访问操作符
使用 []
操作符可以进行索引访问:
1 | interface Person { |
内置工具类型
- Required
将类型的属性变成必选
1 | interface Person { |
- Partial
与 Required 相反,将所有属性转换为可选属性
1 | interface Person { |
从上面知道,如果必传而我们少穿传了的话,就会报错
我们使用 Partial 将其变为可选
1 | type User = Partial<Person> |
- Exclude
Exclude<T, U>
的作用是将某个类型中属于另一个的类型移除掉,剩余的属性构成新的类型
1 | type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" |
- Extract
和 Exclude 相反,Extract<T,U>
从 T 中提取出 U。
1 | type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a" |
适用于:并集类型
- Readonly
把数组或对象的所有属性值转换为只读的,这就意味着这些属性不能被重新赋值。
1 | interface Person { |
- Record
Record<K extends keyof any, T> 的作用是将 K 中所有的属性的值转化为 T 类型。
1 | type Property = 'key1'|'key2' |
- Pick
从某个类型中挑出一些属性出来
1 | type Person = { |
- Omit
与Pick相反,Omit<T,K>
从T中取出除去K的其他所有属性。
1 | interface Person { |
- NonNullable
去除类型中的 null
和 undefined
1 | type P1 = NonNullable<string | number | undefined>; // string | number |
- ReturnType
用来得到一个函数的返回值类型
1 | type Func = (value: string) => string; |
- Parameters
用于获得函数的参数类型所组成的元组类型。
1 | type P1 = Parameters<(a: number, b: string) => void>; // [number, string] |
- InstanceType
返回构造函数类型T的实例类型
1 | class C { |
tsconfig.json
在文章开头环境安装部分,记得我们有生成一个 tsconfig.json 文件,那么这个文件究竟有什么用呢
tsconfig.json 是 TypeScript 项目的配置文件。
tsconfig.json 包含 TypeScript 编译的相关配置,通过更改编译配置项,我们可以让 TypeScript 编译出 ES6、ES5、node 的代码。
重要字段
- files - 设置要编译的文件的名称;
- include - 设置需要进行编译的文件,支持路径模式匹配;
- exclude - 设置无需进行编译的文件,支持路径模式匹配;
- compilerOptions - 设置与编译流程相关的选项。
compilerOptions 选项
1 | { |