前言 📝
本文主要目的是帮助小白快速的上手TypeScript项目,并不会深入的去讲解过多的语法。能够满足平时的基本开发需求。

更深入的AnyScript教程,和我一起学习吧 🚥🚥🚥

  1. 🥪 TypeScript快速入门 ⇦ 当前位置 🪂
  2. 🍚 深入了解TypeScript类型
  3. 🍀 函数与泛型
  4. 🛑 类型编程
  5. 🔫 类和接口
  6. 🚶 装饰器
  7. 🕊️ 工程化

1. 导言

1.1 为什么要使用TypeScript

平时在使用js的时候是运行时错误,只有在运行时候js才知道自己的类型是什么。但是有了TypeScript,他会在我们编译的时候就会提示出错误。还有就是可以帮助我们统一代码规范、风格和质量。

2. TS演练场

playground演练场传送门

不需要我们安装任何环境,可以直接在演练场中测试代码。并直观的看到编译后的代码以及对应的声明文件。

image.png

3. 快速入门

3.1 安装与运行

  1. 建议全局安装typescript
1
npm i -g typescript
  1. 查看安装是否成功
1
tsc --version
  1. 编译ts文件
1
2
# tsc 文件路径
tsc ./src/index.ts

到此为止,我们已经可以进行文件的编译了,但是会不会有点麻烦呢。小编现在是在node环境中进行编写代码的,我发现当我写完ts代码的时候,进行tsc命令编译,将代码生成到对应的目录,我在运行最后的js文件。好麻烦啊,运行一个代码就要经历这么多的步骤。不要急,这里推荐一个npm包。

  1. 建议全局安装ts-node
1
npm i -g ts-node
  1. ts-node编译并运行文件
    ts-node会帮助我们在内存中编译并运行文件,所以在执行ts-node命令的时候,并不会替我们生成编译后的js文件。
1
2
# ts-node 路径
ts-node ./src/index.ts
  1. nodemon
    Nodemon 是一种工具,当检测到目录中的文件更改时,它会自动重新启动 Node 应用程序,从而帮助开发基于Node.js的应用程序。
1
2
3
npm i nodemon -D
# 使用nodemon来监视当前目录(.)下的文件变化,并在检测到变化时执行 ts-node 命令运行 ./src/index.ts文件
nodemon --exec ts-node ./src/index.ts

3.2 配置文件

快速生成配置文件tsc --init

  1. include指定需要编译的文件
    指定需要编译的ts的文件或者目录,默认是匹配我们根目录下面的**/*, **/代表的是匹配递归到任意子目录,*代表的是匹配零个或多个字符(不包括目录分隔符)。我们一般会写成下面这个样子:
1
2
3
4
{
// 匹配src目录下所有文件夹内的ts文件,包括所有的子目录
"include": ["src/**/*.ts"]
}
  1. outDir编译文件的输出目录
    将ts文件变成js文件之后,js文件的存储位置。
1
2
3
4
5
{
"compilerOptions": {
"outDit": "./dist"
}
}
  1. exclude排除编译文件
    include相对,该配置项是要指定需要排除编译的文件,默认值有node_modules、bower_components、jspm_packages、outDir配置的对应目录

  2. target指定编译生成的版本;lib类型库
    target是指定生成js的版本,更改target也会更改lib的默认值。而lib是Ts需要引用的库,即声明文件。

1
2
3
4
5
6
7
8
9
10
11
12
{
// ts需要编译的文件
"include": ["src/**/*.ts"],
"compilerOptions": {
// 编译的版本
"target": "ES2017",
// 找到对应的类型库
"lib": ["ES2017", "DOM", "DOM.Iterable"],
// 编译后的文件输出目录
"outDir": "./dist"
}
}
  1. strict严格模式,开启严格的类型检查
    开启严格类型检查之后,一下的配置会跟随strict:true开启
  • alwaysStrict-在代码中注入use strict,ESM模块化默认就是严格模式,commonJs模块化才会生成
  • nolmplictAny-不允许隐式的any
  • nolmplicitThis-不允许this有隐式的any类型
  • strictBindCallApply-严格的bind\call\apply检查
  • strictFunctionTypes-不允许函数参数双向协变
  • strictNullChecks-不允许把null、undefined赋值给其他类型的变量
  • strictPropertyInitalization-类的实例属性必须初始化
  • useUnkownInCatchVariables-默认catch子句变量为unknow,而不是any

如果tsconfig.json配置文件存在,则在执行tsc命令的时候,不需要在后面添加文件路径了。

3.3 TS常见类型

3.4 any类型

any可以被赋予任何类型的值,直接给变量赋值nullundefined,默认类型是any

3.5 字面量类型

1
const v1 = "hello"
  1. 如果进行类型推导,直接给变量赋值nullundefined,默认类型是any
  2. 因为const声明的变量不能更改,所以默认的类型就是常量字面量类型。

3.6 联合类型

1
2
3
4
5
// 着三种类型都可以
let v2: string|number|boolean

// 只能是男或者女,字面量类型+联合类型
let v3: "男""女"

3.7 数组类型

数组类型可以有两种表示方式:类型[]、Array<类型>

1
2
3
4
5
6
7
8
9
10
const arr1:string[] = ['h', 'e', 'l', 'l', 'o']
const arr2:Array<string> = ['h', 'e', 'l', 'l', 'o']

// 如果不写类型,同时赋值的时候也不写元素,则默认推断出是`any[]`类型
// 如果将配置文件中的`nolmplictAny`配置改为false,他会推断成`never[]`
const arr3 = []

// 数组联合类型
const arr4:(string|number)[] = []
const arr5:Array<string|number> = []

3.8 元组类型

将数组中的每一项,都规定类型

1
2
const tuple1: [string, number] = ["a", 1]
const position: [number, number] = [39.5436, 117.231]

3.9 函数相关

3.9.1 函数参数和返回值

如果不需要返回值,则填写void

1
2
3
4
5
function add(a: number, b: number): string {
return a+b+''
}

add(1,2);

3.9.2 可选参数

可以在某些参数后面加上?,表示参数是非必需传递的。

1
2
3
4
function sum(a: number, b: number, c?:number):number {
return
}
sum(1,2)

可选参数必须要在所有必选参数后面

3.9.3 默认参数

默认参数本身就是可选参数

1
2
3
4
function sum(a: number, b: number, c = 10) {
return a+b+c
}
sum(1,2)

3.9.4 剩余参数

1
2
const fn = (a:number, b: number, ...args: number[]) => {
}

3.9.5 泛型

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
// 不确定是什么类型,需要传递一个类型过来
function log<T>(a: T): T {
console.log(a)
return a
}

log<string>("泛型")


function example<T, K>(a: T, b: K):[T,K] {
return [a, b]
}

example(1, "1")


function myFilter<T>(arr: T[], callback: (item: T, index?:number) => boolean) {
const result = [];
for(let i = 0; i < arr.length; i++) {
if(callback(item, i)) {
result.push(item)
}
}
return result
}

myFilter([1,4,3,5,6], (item) => {
return item % 2 = 0
})

3.10 对象字面量类型

对象字面量类型就是字面量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const v4 = {
name: 'alvis',
age: 1,
id: 130
}

=> 类型推导成

const v4: {
name: string,
age: number,
id: number
} = {
name: 'alvis',
age: 1,
id: 130
}

3.11 自定义类型

3.11.1 类型别名

创建一个类型的新的名字,类型别名可以是任何有效的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// type 名称 = 类型
type Pointer = {
x: number,
y: number
}

type ID = number | string

type Age = number

type User = {
id: ID,
name: string,
age: Age
}

type InfoFn = (id: number, name?: string) => string

3.11.2 接口

接口其实是面向对象的概念,所以一般用于定义对象类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Point {
x: number,
y: number
}

interface Person {
id: ID,
name: string,
age: Age
}

interface Book {
id: number,
name: string,
price: number,
// 函数类型表示方式
show(id: number): void,
filter: (id: number) = void,
info: InfoFn
}

3.12 交叉类型

交叉类型就是将多个类型合并成一个类型。类型A & 类型B、 类型A | 类型B

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
type A = {
id: number,
name: string
}

type B = {
age: number
}

type C = A & B

// A和B的任意一个属性都不能少,必须要全部符合A、B两个类型
const objs: C = {
id: 1,
name: 'alvis',
age: 18
}

type D = A | B

// 同时满足类型A和类型B也可以
const obj: D = {
// 只满足类型A的类型也可以
id: 1,
name: 'alvis',
// 只满足类型B的类型也可以
age: 18
}

3.13 类型断言

简单来说,TS会根据上下文进行推测,但是有时候我们可以认为干涉,确定某一个类型。类型断言就是告诉TS编译器,我知道我在做什么,这里没有类型安全问题,我自己来掌握,允许我们使用更宽松的方式处理类型问题。

语法: 值 as 类型 或者 <类型>值

1
2
3
let someValue: any = "this is a string"
let strLeng1 = (someValue as string).length
let strLeng2 = (<string>someValue).length

3.14非空断言

当你确定某个值不是null或者undefined的时候,可以直接使用非空断言。

语法:值!

1
2
3
let maybeString: string | undefined = "hello"
// 如果不加入非空断言,此时的类型可能是 string | undefined,加上非空断言就确定了此时不是undefined
let defineString = maybeString!

3.15 可选链操作符(js语法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Address {
city?: string
street?: string
}

interface Student {
name: string
address?: Address
}

const student: Student = {
name: 'alvis',
address: {
city: "河北"
}
}

// address.street可能不存在,如果不存在就直接返回undefined,不再往后走了
let street = student.address?.street

4. 类型声明

在前面的代码中,我们说从 typescript 编译到 Javascript 的过程中,类型消失了,比如下面的代码:

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
const str = "hello";
type User = {
id: number
name: string
show?: (id: number, name: string) => void
}

const u:User = {
id:1,
name:"张三",
show(id,name){
console.log(id,name)
}
}

const users:Array<User> = [
{id:1,name:"jack"},
{id:2,name:"rose"}
]

function addUser(u:User){
// todos...
return true;
}

addUser(u);

编译成javascript之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"use strict";
const str = "hello";
const u = {
id: 1,
name: "张三",
show(id, name) {
console.log(id, name);
}
};
const users = [
{ id: 1, name: "jack" },
{ id: 2, name: "rose" }
];
function addUser(u) {
// todos...
return true;
}
addUser(u);

但是是真的消失了吗?其实并不是,如果大家留意之前我们在Playground上编写代码,专门有一项就叫做DTS

你会发现,我们写的代码都自动转换成了typescript类型声明。

当然,这在我们的VS Code编辑器中也能生成的。只需要在tsconfig.json文件中加上相关配置即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"compilerOptions": {
"target": "es2020",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist",
+ "declaration": true,
+ "declarationDir": "./types",
},
"include": ["src/**/*.ts"],
"exclude": ["./node_modules", "./dist", "./types"]
}

运行tsc,最后生成:[文件名].d.ts

1
2
3
4
5
6
7
8
9
declare const str = "hello";
type User = {
id: number;
name: string;
show?: (id: number, name: string) => void;
};
declare const u: User;
declare const users: Array<User>;
declare function addUser(u: User): boolean;

也就是说,类型并不是真的全部消失了,而是被放到了专门的类型声明文件里。

.d.ts结尾的文件,就是类型声明文件。d的含义就是declaration

其实typescript本身就包含两种文件类型

1、.ts文件:既包含类型信息,又包含可执行代码,可以被编译成.js文件后执行,主要是我们编写文件代码的地方

2、.d.ts文件:只包含类型信息的类型声明文件,不会被编译成.js代码,仅仅提供类型信息,所以类型文件的用途就是提供类型信息

4.1 类型声明文件的来源

类型声明文件主要有以下三种来源。

  • TypeScript 编译器自动生成。
  • TypeScript 内置类型文件。
  • 外部模块的类型声明文件,需要自己安装。

4.1.1自动生成

只要使用编译选项declaration,编译器就会在编译时自动生成单独的类型声明文件。

下面是在tsconfig.json文件里面,打开这个选项。

1
2
3
4
5
{
"compilerOptions": {
"declaration": true
}
}

declaration这个属性还与其他属性有强关联:

4.1.2 内置声明文件

安装 TypeScript 语言时,会同时安装一些内置的类型声明文件,主要是内置的全局对象(JavaScript 语言接口和运行环境 API)的类型声明。这也就是为什么stringnumber等等基础类型,Javascript的api直接就有类型提示的原因

内置声明文件位于 TypeScript 语言安装目录的lib文件夹内

这些内置声明文件的文件名统一为lib.[description].d.ts的形式,其中description部分描述了文件内容。比如,lib.dom.d.ts这个文件就描述了 DOM 结构的类型。

如果想了解对应的全局对象类型接口,可以去查看这些内置声明文件。

tsconfig.json中的配置targetlib其实就和内置声明文件是有关系的。TypeScript 编译器会自动根据编译目标target的值,加载对应的内置声明文件,默认不需要特别的配置。我们也可以指定加载哪些内置声明文件,自定义配置lib属性即可:

1
"lib":["es2020","dom","dom.iterable"]

为什么我们没有安装typescript之前也有提示?

这是由于我们的VS Code等IDE工具在安装或者更新的时候,已经内置了typescript的lib。一般在你的VS Code安装路径 -> resources -> app -> extensios -> node_modules -> typescript

如果你的VS Code一直没有升级,就有可能导致本地VS Codetypescript版本跟不上的情况,如果你的项目目录下,也安装的的有typescript,我们是可以进行切换的。

VS Code中使用快捷键ctrl(command) + shift + P,输入TypeScript

选择Select Typescript Version...

你可以选择使用VS Code版本还是项目工作区的版本

4.2 外部类型声明文件

如果项目中使用了外部的某个第三方库,那么就需要这个库的类型声明文件。这时又分成三种情况了。

1、第三方库自带了类型声明文件

2、社区制作的类型声明文件

3、没有类型声明文件

没有类型声明这个很容易理解,我们现在不纠结这种情况,而且大多数情况下,我们也不应该去纠结他,关键是1,2两点是什么意思?其实我们下载两个常用的第三方库就能很明显的看出问题。

1
npm i axios lodash

注意:引入模块之前,涉及到模块的查找方式,因此在tsconfig.json中需要配置**module**

对于现代 Node.js 项目,我们可以配置NodeNext,注意这个配置会影响下面的配置:

1
2
"moduleResolution": "NodeNext",
"esModuleInterop": true

当然,具体模块化的配置,不同的环境要求是不一样的,有一定的区别,比如是nodejs环境,还是webpack的打包环境,或者说是在写一个第三方库的环境,对于模块化的要求是不一样的。而且还涉及到模块化解析方式等问题。这里就先不详细深入讲解了

nodejs环境下,我们先简单配置为"module":"NodeNext"

webapck/vite等打包环境下,设置为:

"module": "ESNext"

"moduleResolution": "bundler"

引入相关模块:

其实打开这两个库的源代码就能发现问题,axios是有.d.ts文件的,而lodash没有,也就是说根本没有类型声明,那当然就和提示的错误一样,无法找到模块的声明文件。

第三方库如果没有提供类型声明文件,社区往往会提供。TypeScript 社区主要使用 DefinitelyTyped,各种类型声明文件都会提交到那里,已经包含了几千个第三方库。上面代码提示的错误,其实就是让我们到@types名称空间去下载lodash对应的类型声明,如果存在的话。当然,你也可以到npm上进行搜索。几乎你知道的所有较大的库,都会在上面找到,所以一般来说也要下载或者搜索都比较简单,@types开头,/后面加上第三方库原来的名字即可,比如:

@types/lodash@types/jquery@types/node@types/react@types/react-dom等等

1
npm i --save-dev @types/lodash
1
2
3
4
import lodash from 'lodash'

const result = lodash.add(1, 2);
console.log(result)

默认情况下,所有可见的“@types”包都会包含在你的编译中。任何包含文件夹中node_modules/@types的包都被视为可见。“任何包含文件夹”意味着不仅是项目的直接node_modules/@types目录会被搜索,上层目录中的相应文件夹也会被递归搜索

可以通过typeRoots选项设置查找的文件路径,如果指定了typeRoots,则只会包含typeRoots下的包。例如:

1
2
3
4
5
{
"compilerOptions": {
"typeRoots": ["./typings", "./vendor/types"]
}
}

这个配置文件将会包含./typings./vendor/types下的所有包,但不会包含./node_modules/@types下的任何包。所有路径都是相对于tsconfig.json文件的。

也就是说,如果你要手动指定typeRoots,那就需要自己手动指定所有需要查找的目录,如果你的项目中有深层次的目录结构,并且你希望包含其中的类型声明,你需要确保这些目录都被明确地添加到typeRoots中。

其实,nodejs本身也没有TypeScript的类型声明,因此你会发现在.ts文件中直接引入nodejs相关的模块同样会报错

1
import path from "path"; // error 找不到模块"path"或其相应的类型声明

同样,我们直接在DefinitelyTyped下载即可

1
npm i @types/node -D

5. 类型声明文件的用途

我们自己当然也能编写类型声明文件,但是声明文件.d.ts大多数时候是第三方库一起使用的,我们写代码在nodejs环境下,单独去声明.d.ts文件没有太大的意义,首先大家要知道这个问题。所以,要使用.d.ts声明文件的场景一般是:

1、自己写了一个主要是Javascript代码的第三方库,需要给这写Javascript代码加上类型声明,以便用户使用的时候可以得到类型声明,方便调用API。

2、自己下载了别人写的第三方库,但是没有typescript类型声明,在社区 DefinitelyTyped中也没有找到对应的类型声明,但是我们一定要用这个库,可以手动为这个库添加一些简单的类型声明,以免我们自己项目在使用这个第三方库没有类型声明报出错误提示。

3、在做应用项目的时候,需要补充一些全局的类型声明的时候,我们可能需要自己动手写.d.ts文件,其实这种情况大多数还是和第2点有关系