Typescript
TypeScript
是Javascript
的超集,遵循最新的ES5/ES6
规范。Typescript
扩展了Javascript
语法。
环境配置
安装
typescript
对ts
编译1
2
3
4
5
6
7
8npm i typescript -g
tsc --init # 生成 tsconfig.json
tsc # 可以将 ts 编译成 js
tsc --watch # 监听 ts 文件变化生成 js,但是每次都会生成 js 文件
npm i ts-node -g # 可以运行 .ts 文件,或者 vscode 下载 code runner 插件,内部也是通过 ts-node 来执行的。
ts-node a.ts # 直接运行 ts配置
ts
环境(此处使用 rollup)安装依赖
1
npm i rollup typescript rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-serve -D
初始化
ts
配置文件(因为tsc
不知道要转化成什么格式的,需要生成一个tsconfig.json
来告诉它需要什么样的格式)1
npx tsc --init
target
是打包目标,module
是导出的模块规范,不需要babel
了。由于前端打包模块时会有tree-shaking
所以不能用 commonjs 规范,一般用 ESNextwebpack
配置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// rollup.config.js
import ts from 'rollup-plugin-typescript2'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import serve from 'rollup-plugin-serve'
import path from 'path'
export default {
input: 'src/index.ts',
output: {
format: 'iife',
file: path.resolve('dist/bundle.js'),
sourcemap: true // tsconfig.json 里的 sourcemap 也要打开才能有效
},
plugins: [
nodeResolve({
extensions: ['.js', '.ts']
}),
ts({
tsconfig: path.resolve(__dirname, 'tsconfig.json')
}),
serve({
open: true, // 自动打开
openPage: '/public/index.html',
port: 3000,
contentBase: '' // 表示路径,如果值为 '1' ,路径就会变成/practice/1/public/index.html,该插件必须要加 contentBase
})
]
}
每个 .ts 文件都要导出,防止被认为是全局变量,只要 export 了之后,它就会被认为是模块,不会与内置变量冲突,如
1
2
3let name
export {} // 不加这行就报错,因为 ts 内置了 name 类型为 void,不导出被认为是与内置的 name 冲突了,无法重新声明变量 name。
基础类型
TS中冒号后面的都为类型标识。
标识实例才用大写,其他时候都是小写的。
布尔、数字、字符串类型
1
2
3
4
5
6let bool: boolean = true
let num: number = 1
let str: string = 'hello world'
// 1..toString() 相当于 Number(1).toString() 或 (1).toString(),这样写相当于 1.0.toString(),因为数字之后的 . 无法判断是小数点还是取值,写两个点就表示第一个是小数点第二个是取值
// {} + [] 的值,如果是 console.log({} + []),会对它们做 toString(),{} 变成 [object Object],[] 变成 '',所以是 [object Object],如果单纯在浏览器的控制台里输入 {} + [],则 {} 会被解析成作用域,也就是和不存在这个东西一样,只剩下 + [],那么就是 +'' 就是 0.元组类型
限制长度个数,类型一一对应
1
2
3
4let tuple: [string, number, boolean] = ['dl', 1, true]
// 初始化时,元组的数据类型顺序必须和定义的类型顺序一致,如果初始赋值 ['dl', true, 1] 就会报错
// 向元组中添加数据,只能增加元组中存放的类型
tuple.push('123', false, 0)数组类型
1
2
3
4let arr1: number[] = [1, 2, 3]
let arr2: string[] = ['1', '2', '3']
let arr3: (number | string)[] = [1, '2', 3]
let arr4: Array<number | string> = [1, '2', 3] // 泛型方式声明枚举类型
可以枚举,也可以反举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68enum USER_ROLE {
USER, // 默认下标为 0,依次递增,互相对应
ADMIN, // 1
MANAGER // 2
}
/*
题外:0,1,2 分别对应 0000,0001,0010, 0, 1, 1<<1,到时候如果同时有 admin 和 manager 的权限,可能西城 ADMIN & MANAGER ,类似单片机,得到结果为 0011
实际结果
{
0: 'USER',
USER: 0,
1: 'ADMIN',
ADMIN: 1,
2: 'MANAGER',
MANAGER: 2
}
// 编译后的结果
var USER_ROLE;
(function (USER_ROLE) {
USER_ROLE[USER_ROLE["USER"] = 0] = "USER";
USER_ROLE[USER_ROLE["ADMIN"] = 1] = "ADMIN";
USER_ROLE[USER_ROLE["MANAGER"] = 2] = "MANAGER";
})(USER_ROLE || (USER_ROLE = {}));
*/
// 异构枚举
enum USER_ROLE {
USER = 'user',
ADMIN = 1,
MANAGER
}
let manager: USER_ROLE = USER_ROLE.MANAGER // 定义类型,不能写成 let manager: USER_ROLE = 2 这种常量,一定要从枚举中取,否则报错
/*
实际结果
{
USER: 'user',
1: 'ADMIN',
ADMIN: 1,
2: 'MANAGER',
MANAGER: 2
}
// 编译后的结果
var USER_ROLE;
(function (USER_ROLE) {
USER_ROLE["USER"] = "user";
USER_ROLE[USER_ROLE["ADMIN"] = 1] = "ADMIN";
USER_ROLE[USER_ROLE["MANAGER"] = 2] = "MANAGER";
})(USER_ROLE || (USER_ROLE = {}));
*/
// 常量枚举,一般情况下用这种,在不需要反举的情况下,使用这种,不会编译出额外的代码。
const enum USER_ROLE {
USER,
ADMIN,
MANAGER,
}
console.log(USER_ROLE.USER)
console.log(USER_ROLE.ADMIN)
console.log(USER_ROLE.MANAGER)
// 实际结果, USER_ROLE 这个对象不会成为一个变量
// console.log(0 /* USER */);
// console.log(1 /* ADMIN */);
// console.log(2 /* MANAGER */);any 类型
放弃 ts 的类型检测
1
2
3let str: any = '1'
str = 1
let arr: any = ['dl', true, { hello: 'world' }]null 和 undefined
任何类型的子类型,如果
strictNullChecks
(开启 null 的严格检测)的值为 true, 则不能把 null 赋给其他类型1
2
3
4
5let name: number | boolean
name = null
let un: undefined = undefined // 只能赋值 undefined,不能 null
let nu: null = null // 只能 null,不能undefinedvoid 类型
只接受 null, undefined。一般用于函数的返回值,不接受 null。
1
2let a: void
a = undefinednever 类型
任何类型的子类型,never 代表不会出现的值。不能把其他类型赋值给 never
1
2
3
4
5
6
7
8
9
10
11
12
13
14function error(message: string): never {
throw new Error('err')
}
function loop(): never {
while(true) {}
}
function fn(x: number | string) {
if (typeof x === 'number') {}
else if (typeof x === 'string') {}
else {
// never ,此时表示传入的 x 不是声明的两种类型之一,即这段逻辑是不会触发的,因此就是 never
console.log(x)
}
}Symbol 类型
表示独一无二
1
2
3const s1: symbol = Symbol('key')
const s2: symbol = Symbol('key')
s1 == s2 // falseBigInt 类型
与
number
类型不兼容1
2
3
4
5const num1: number = Number.MAX_SAFE_INTEGER + 1 // 定义为 bigint 会报错
const num2: number = Number.MAX_SAFE_INTEGER + 2
num1 == num2 // true
let max: bigint = BigInt(Number.MAX_SAFE_INTEGER)
max + BigInt(1) === max + BigInt(2) // falseObject 对象类型
object
表示非原始类型1
2
3
4let create = (obj: object): void => {}
create({})
create([])
create(function () {})
类型推导
类型推导
声明变量没有赋予值时默认变量是
any
类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let name // 类型为 any
name = 'dl'
name = 10
// 声明变量赋值时以赋值类型为准
let name = 'dl' // name 被推导为字符串类型
name = 10 // error
+ #### 包装对象
我们在使用基本数据类型时,调用基本数据类型上的方法,默认会将原始数据类型包装成对象类型。
```typescript
let bool1:boolean = true;
let bool2:boolean = Boolean(1);
let bool3:Boolean = new Boolean(2);boolean是基本数据类型 , Boolean是他的封装类
联合类型
在使用联合类型时,没有赋值只能访问联合类型中共有的方法和属性。(注:| 是并集,只需要满足部分即可,& 是交集,全都要满足)
1
2
3
4
5
6
7let name:string | number // 联合类型
console.log(name!.toString()); // 公共方法
name = 10;
console.log(name!.toFixed(2)); // number方法
name = 'zf';
console.log(name!.toLowerCase()); // 字符串方法
// ! 表示非空断言非空断言
! 表示非空断言,表示用户告诉编辑器一定有值,放弃空值检测,但一定是已经定义的其他类型,如下面的 ele 使用非空断言后一定是 HTMLElement,而不可能是 string, number 等类型。
1
2let ele: HTMLElement | null = document.getElementById('#app');
ele!.style.color = 'red'; // 断定ele元素一定有值
类型断言
类型断言
1
2
3let name: string | number;
(name! as number).toFixed(2); // 强制
((<number>name!).toFixed(2));尽量使用第一种类型断言因为在 react 中第二种方式会被认为是
jsx
语法双重断言
1
2let name: string | boolean;
((name! as any) as string);尽量不要使用双重断言,会破坏原有类型关系,断言为 any 是因为 any 类型可以被赋值给其他类型
字面量类型
可以用字面量当做类型,同时也表明只能采用这几个值(限定值)。类似枚举。
1
2type Direction = 'Up' | 'Down' | 'Left' | 'Right';
let direction:Direction = 'Down';
函数类型
函数的两种声明方式
通过 function 关键字来进行声明,注意 function 不能使用下面的表达式声明。
1
2
3
4function sum(a: string, b: string): string {
return a + b;
}
sum('a','b')可以用来限制函数的参数和返回值类型
通过表达式方式声明
1
2
3
4type Sum = (a1: string, b1: string) => string;
let sum: Sum = (a: string, b: string) => {
return a + b;
}
可选参数
1 | let sum = (a: string, b?: string): string => { |
默认参数
1 | let sum = (a: string, b: string = 'b'): string => { |
剩余参数
1 | const sum = (...args: string[]): string => { |
函数的重载
1 | function toArray(value: number): number[] |
类
TS中定义类
1 | class Pointer { |
实例上的属性需要先声明在使用,构造函数中的参数可以使用可选参数和剩余参数
类中的修饰符
public (默认值)意味着公开,自己能访问,儿子(extends)能访问,外界能访问(new XXX()、XXX.xxx)
protected 自己能访问,儿子能访问,外界不能访问
private 只能自己访问,可以通过属性访问器来修改或访问这个属性
readonly 表示这个属性是只读的,只能在初始化的时候修改
构造函数 constructor 也可以添加修饰符,除了 public 之后都不能 new 这个类
super 在构造函数和静态方法中,super 指向父类(XXX),在原型方法中,指向原型(XXX.prototype)
子类改写父类的原型方法(public),返回的值要兼容,父类返回 string,子类也要返回 string
静态属性和方法
static 静态属性和静态方法是可以被子类所继承的
子类改写父类的静态方法,返回的值要兼容,父类返回 string,子类也要返回 string
1 | class Animal { |
类的装饰器
装饰类
类装饰器可以装饰类本身(参数就是类的原型)、类的属性、类的方法、静态属性、类中函数的参数都可以使用,装饰器是语法糖,目前是试验性的,需要到 tsconfig.json 中打开 expoerimetalDecorators 为 true 才能使用,后续有可能会更改。
1 | function addSay(target: Function) { // 标记原型可以写 Function |
抽象类
抽象类无法被实例化(无法 new),只能被继承,抽象方法不能在抽象类中实现,只能在抽象类的具体子类中实现,而且必须实现。
1 | abstract class Animal { |
定义类型时 void
表示函数的返回值为空(不关心返回值类型,所有在定义函数时也不关心函数返回值类型)
接口
接口在开发中会被大量使用,可以用来描述对象的形状。
一般以 I 开头
1 | // type 可以定义联合类型, interface 不可以 |
接口继承
implements 可以传入一个列表,表示这个类要满足这个列表中类的方法,有具体的实现
1 | interface Speakable { |
构造函数类型
类只能描述实例,当我们要描述类本身的时候,以下面代码为例,
typeof Cat
可以取这个类的类型,注意,typeof 只能对已有的类型的类型,不能对 T 这种泛型取类型
new (name: string, age: number) => Cat
表示是一个构造函数,我们限制返回的实例,new() 表示当前是一个构造函数类型。
1 | type MyType = new (name: string, age: number) => Cat |
泛型
泛型的特点,就是解决在定义的时候不能明确类型,只能在使用的时候才能明确类型的情况。
用到泛型的地方前面一定要定义泛型的这个变量名
引子
在上面的构造函数类型中,如果 createInstance 的 clazz 类型不确定时,需要动态传入类型,就用上泛型,否则仅会定义为 Cat,比如我想传入 Dog 时被定义为 Dog,传入 Cat 被定义为 Cat
1 | type MyType<T> = new (name: string, age: number) => T |
多个泛型
1 | function swap<A, B>(tuple: [A, B]): [B, A] { // 交换元组类型 |
默认泛型
用默认值来规定泛型的不确定性,不传递参数默认值是 unknown
1 | interface TStr<T = string> { |
泛型形参的位置
在写类型时如果泛型定义在函数的前面(<T>(param1){}
),表示的是执行的时候确定类型,如果放在接口或者 type 的后面(IFn<T>, TFn<T>
),表示使用这个类型的时候就定义好了具体类型。
比较以下几种写法
1 | // 声明时直接确定类型 比如 fn: IFn<number>,就被确定为 number 了,由于这里的 forEach 前面也有个 <T> ,导致看起来整体是运行时确定类型,实际上过 fn 的话,是再声明时候就确定了。 |
泛型约束
泛型必须包含某些属性,就是泛型约束。
1 | function sum<T extends string>(a: T, b: T): T { |
keyof
我们期望在一个对象(类型的意思)能获取对象(类型)中所有的属性,可以用 keyof 关键字。返回泛型中指定属性,是一个联合类型,并集。
1 | function getVal(T extends object, K extends keyof T)(val: T, key: K) {} |
特殊且常用:
type r = keyof any // string | number | symbol
因为这三个都能作为 key。
typeof
取出变量的类型,和 js 中的 typeof 不同,ts 的typeof 得到的是类型,js 得到的是值。
1 | let val = { a: 1, b: 2 } |
交叉类型
表示两者共有的部分
1 | interface A { |
条件类型
ts 中也可以使用三元表达式。
1 | interface Bird { |
内置类型
都可以自己实现,以下为自行实现。前几个类型依赖条件类型。
Exclude
传入两个类型,从第一个类型中排除第二个类型后返回最后的类型。
在多的类型中去掉某个类型,可以考虑使用。
1 | type Exclude<T, U> = T extends U ? never: T |
Extract
传入两个类型,从第一个类型选中中第二个类型中也存在的类型后返回。
1 | type Extract<T, U> = T extends U ? T : never |
NonNullable
去除 null 和 undefined 类型。
1 | let el = document.getElementById('app') |
Partial
让外层属性变成可选属性。 ts 中也有 key in keys 这样的循环。
1 | interface ICompany { |
Required
让外层属性变成必填项。- 是 ts 的操作符,可以去掉一些操作符。
1 | type Required<T> = { [K in keyof T]-?: T[K] } // 和 Partial 实现基本一致 |
Readonly
让外层属性变成仅读项。- 是 ts 的操作符,可以去掉一些操作符。
1 | type Readonly<T> = { readonly [K in keyof T]: T[K] } // 和 Partial 实现基本一致 |
Pick
挑选出想要的结果,传入两个类型,第一个类型为需要被挑选的接口(对象),第二个为要被挑选的接口上的属性。
1 | interface IPerson { |
Omit
忽略属性,传入两个类型,第一个类型为需要被忽略的接口(对象),第二个为要被忽略的属性(可以写不是第一个接口上的属性,这和 Pick 不同)。
1 | interface IPerson { |
Record
可以描述对象类型,是键值对。
1 | type Record<T extends keyof any, K> = { |
ReturnType
推断函数的返回值。 infer 在哪里就推断哪里的结果,下面的 infer 在返回值的地方,因此推断返回值。
1 | function getPoint(x: string, y: string, z: string) { |
Paramaters
推断属性。看看 infer 在属性位置,和上面的 returntype 对比一下,大概就能知道 infer 的用法了。
1 | function getPoint(x: string, y: string, z: string) { |
ConstructorParameters
推断类上的属性。同理,用 infer 来推断,不一样的是,我们需要用 new 来标志这个类来 extends,和用上面的函数来 extends 一样。
1 | class Person { |
InstanceType
推断实例,好像没什么用,要实例的类型,不就是类的类型吗。。
1 | type InstanceType<T extends { new (...args: any[]): any }> = T extends { new (...args: any[]) => infer R } ? R : never |
unknown
unknown 可以认为 是 any 的安全类型。
unknown 不能进行取值操作,
和任何类型做联合类型,都是 unknown,
和其他类型做交集得到的是其他类型,
不支持 keyof unknown(即 unknown 类型不能被遍历)
never 是 unknown 的子类型
never extends unknown
keyof unknown 是never
1 | let a: unknown = { a: 1 } |
交、叉、并集
自行实现交、叉、并集和展开类型(Compute),Diff,Overwrite。
1 | const person1 = { |
类型保护
类型保护能让 ts 更好的去识别类型,js 本身就有类型识别功能,如 typeof、instanceof、in、?,可以用于类型保护,当我们使用 js 的这些关键词的时候,ts 会自动识别并进行类型保护。
js 中 typeof、instanceof、in、? 以及 ts 中 is 的保护
1 | // typeof |
完整性保护
never 的用法有一个很经典的例子,有点像 C++ 中的断言,当状态判断情况缺少的时候就会报错。
1 | interface ISquare { |
ts 兼容性
鸭子类型检测:范围小的可以赋予范围大的,范围大的不能赋予范围小的,只要满足类型条件即可,不关注内容。
接口类型兼容
1 | let v1!: string | number |
函数兼容
参数和返回值都兼容情况下才不会报错。
1 | // 参数兼容 |
类的类型兼容
鸭子检测,只要实例属性类型一样,那就是一个东西(不会检测原型链,因为这是在检测值而不是类型,ts 仅检测类型)
特殊情况是,如果类中带了 private、protected 等修饰符,就不是一个东西了,不再兼容(默认属性是 public 可以兼容)。
1 | class Person {} |
枚举兼容
枚举永远不兼容
1 | enum E1 {} |
泛型兼容
泛型是否兼容看最终的结果
1 | interface I1<T> {} |
逆变和协变
逆变和协变是针对函数(传入参数为函数的高级函数)的抽象概念,主要是参数与返回值的父子关系。
可以用来记忆函数的类型兼容性。
参数是逆变的,返回值是协变的,可以返回儿子(总结就是传父返子,即需要传入大范围的参数返回小范围的结果的函数)。
如果配置 ts 的话,可以设置双向协变(但是一般不开,比较诡异)。
1 | class GrandParent { |
命名空间
为了解决命名冲突问题,
命名合并问题,能合并的东西有接口同名可以合并,函数和命名空间可以合并,命名空间和命名空间可以合并,命名空间和类可以合并,(类还可以和接口合并)
1 | namespace Zoo1 { |
declare
declare 是声明的意思。
如果是全局的变量扩展,需要使用 declare global,否则无法访问
1 | // 无效 |
声明文件
ts 下安装第三方包,必须有第三方包的类型声明,否则自己添加一个包含 declare module thirdPackage 的 .d.ts 文件。
很多第三方包都已经有人写好了声明文件,我们只需要引入即可 npm i @types/jquery -S
.
当前代码会默认先查找 node_modules 下的某个模块的 package.json 中是否有 types 字段,如果没有,会去找目录下的 index.d.ts,如果也没有,就会去找 node_modules/@types 下的文件(比如我要用 jq,就是找 @types/jquery),默认去找 index.d.ts 文件。
自定义模块声明
1 | declare module jquery // 写在 typings/index.d.ts 或其他 .d.ts |
自定义的声明路径
ts 的 declare 一般情况下,第三方模块写在根目录下的 typings/index.d.ts 中,相对路径的写到根目录下的 types/index.d.ts 中,都需要对 ts.config.json 做配置。
- 第三方模块需要在 compilerOptions 同级属性下新增
"include": ["typings/**/*"]
,**/*
表示该目录所有深度下的 .d.ts 都会被读取。 - 相对路径的引入需要配置 compilerOptions 属性中的
baseUrl: "./"
(告诉 ts 以哪个目录解析)、paths:{ "*": ["types/*"] }
,设置完这个写自己的类型就可以不用在模块里到处import type {xxx} from 'xxx'
了,已经自动引入了。
1 | { |
兼容 commonJS
因为在 ts 中,使用 module.exports = xxx 或 module = xxx 或 require 都会使得类型变成 any,因此ts 中为了兼容 commonJS,存在特殊写法。(ts 中使用 import 和 export 会自动推导类型)
1 | // 2.ts |
/// 指令
很老的写法,三斜杠指令表示打包的时候引入这些声明。
1 | /// <reference path="JQuery.d.ts"> |
declaration
在 ts.config.json 中有 declaration 属性,将其设置为 true,在打包后会根据类型自动生成 .d.ts 文件
1 | let a: string = '123' |