文章

前端开发整理(二): TypeScript语言

前端开发整理(二): TypeScript语言

TypeScript简介和需求原因

TypeScript 简称 TS ,是在 JavaScript 的基础上增加了一套类型系统,它支持所有的 JS 语句,为工程化开发而生,最终在编译的时候去掉类型和特有的语法,生成 JS 代码。

对于下述JavaScript代码:

1
2
3
4
5
6
7
function getFirstWord(msg) {
  console.log(msg.split(' ')[0])
}

getFirstWord('Hello World') // 输出 Hello

getFirstWord(123) // TypeError: msg.split is not a function

这里定义了一个用空格切割字符串的方法,并打印出第一个单词;但第二次执行时,由于数值不存在 split 方法,所以传入 123 引起了程序崩溃。因此对于传统的JavaScript,需要在split前执行一层判断或转换,工作量增加;因此使用TypeScript,在编译的时候就可以执行检查来避免掉这些问题,而且配合 VSCode 等编辑器的智能提示,可以很方便的知道每个变量对应的类型

Hello TypeScript

运用TypeScript语言的准备

文件目录保持类似,在src文件夹下创建ts文件夹归类本次测试文件,然后创建index.ts文件在ts文件夹下:

1
2
3
4
5
6
7
8
9
10
11
hello-node
│ # 源码文件夹
├─src
│ │ # 业务文件夹
│ └─cjs
│   │ # 入口文件
│   ├─index.cjs
│   │ # 模块文件
│   └─module.cjs
│ # 项目清单
└─package.json

然后在命令行通过 cd 命令进入项目所在的目录路径,安装 TypeScript 开发的两个主要依赖包:npm install -D typescript ts-node(这次添加 -D 参数,因为 TypeScript 和 TS-Node 是开发过程中使用的依赖,所以将其添加到 package.json 的 devDependencies 字段里)

  • typescript 这个包是用 TypeScript 编程的语言依赖包
  • ts-node 是让 Node 可以运行 TypeScript 的执行环境

然后修改 scripts 字段,增加一个 dev:ts 的 script :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "name": "hello-node",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "...": "...",
    "dev:ts": "ts-node src/ts/index.ts",
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "md5": "^2.3.0"
  },
  "devDependencies": {
    "ts-node": "^10.7.0",
    "typescript": "^4.6.3"
  }
}

TypeScript测试

下述三段代码分别产生三个结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/ts/index.ts
// 第一段代码
function getFirstWord(msg) {
  console.log(msg.split(' ')[0])
}
getFirstWord('Hello World')
getFirstWord(123)

// 第二段代码
function getFirstWord(msg: string) {
  console.log(msg.split(' ')[0])
}
getFirstWord('Hello World')
getFirstWord(123)

// 第三段代码
// src/ts/index.ts
function getFirstWord(msg: string) {
  console.log(msg.split(' ')[0])
}
getFirstWord('Hello World')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 第一个结果
TSError: ⨯ Unable to compile TypeScript:
src/ts/index.ts:1:23 - error TS7006: Parameter 'msg' implicitly has an 'any' type.

1 function getFirstWord(msg) {

# 第二个结果
TSError: ⨯ Unable to compile TypeScript:
src/ts/index.ts:7:14 - error TS2345:
Argument of type 'number' is not assignable to parameter of type 'string'.

7 getFirstWord(123)
               ~~~

# 第三个结果
npm run dev:ts

> demo@1.0.0 dev:ts
> ts-node src/ts/index.ts

Hello

常用的TypeScript类型定义

原始数据类型

原始数据类型是一种既非对象也无方法的数据,常见的JS和TS类型对比如下:

原始数据类型 JavaScript TypeScript
字符串 String string
数值 Number number
布尔值 Boolean boolean
大整数 BigInt bigint
符号 Symbol symbol
不存在 Null null
未定义 Undefined undefined

语法解析:

1
2
3
4
5
6
7
8
const <变量名> <数据类型> = <具体内容>

// 字符串
const str: string = 'Hello World'
// 数值
const num: number = 1
// 布尔值
const bool: boolean = true

数组类型

除了原始数据类型之外, JavaScript 还有引用类型,数组 Array 就是其中的一种;其在 TS 类型定义的写法中,是非常接近原始数据的一个类型。其写法如下;也可以省略类型,因为TS同样会推导其类型:

数组里的数据 类型写法 1 类型写法 2
字符串 string[] Array<string>
数值 number[] Array<number>
布尔值 boolean[] Array<boolean>
大整数 bigint[] Array<bigint>
符号 symbol[] Array<symbol>
不存在 null[] Array<null>
未定义 undefined[] Array<undefined>

如下述语法:

1
2
3
4
5
6
7
8
// 字符串数组
const strs: string[] = ['Hello World', 'Hi World']

// 数值数组
const nums: number[] = [1, 2, 3]

// 布尔值数组
const bools: boolean[] = [true, true, false]

注意:如果开始数组为空,则无法自动识别为数组类型而产生报错风险,即使在后续进行入或出的操作都是错误的。

对于复杂的数组,比如数组里面的 item 都是对象,其实格式也是一样,只不过把原始数据类型换成对象的类型即可,例如 UserItem[] 表示这是一个关于用户的数组列表。

对象(接口)

对象也是引用类型,在 TypeScript ,类型定义需要根据值的类型来确定它的类型。定义对象可以通过typeinterface进行,先进行interface的学习,其写法与 Object 更为接近并被用得更多。

注意点
对象的类型定义通常采用 Upper Camel Case 大驼峰命名法,也就是每个单词的首字母大写,例如 UserItem 、 GameDetail ,这是为了跟普通变量进行区分(变量通常使用 Lower Camel Case 小驼峰写法,也就是第一个单词的首字母小写,其他首字母大写,例如 userItem )。

以用户信息例,比如要描述 Petter 这个用户,他的最基础信息就是姓名和年龄,那么定义为接口就是这么写:

1
2
3
4
5
6
7
8
9
10
11
// 定义用户对象的类型
interface UserItem {
  name: string
  age: number
}

// 在声明变量的时候将其关联到类型上
const petter: UserItem = {
  name: 'Petter',
  age: 20,
}

可选接口属性

上面这样定义的接口类型,表示 nameage 都是必选的属性,不可以缺少,一旦缺少,代码运行起来就会报错。在实际的业务中,有可能会出现一些属性并不是必须的,就像这个年龄,可以将其设置为可选属性,通过添加 ? 来定义,格式为?!

1
2
3
4
5
6
7
8
9
interface UserItem {
  name: string
  // 这个属性变成了可选
  age?: number
}

const petter: UserItem = {
  name: 'Petter',
}

调用自身接口的属性

如果一些属性的结构跟本身一致,也可以直接引用,案例如下:

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
interface UserItem {
  name: string
  age: number
  enjoyFoods: string[]
  // 这个属性引用了本身的类型
  friendList: UserItem[]
}

const petter: UserItem = {
  name: 'Petter',
  age: 18,
  enjoyFoods: ['rice', 'noodle', 'pizza'],
  friendList: [
    {
      name: 'Marry',
      age: 16,
      enjoyFoods: ['pizza', 'ice cream'],
      friendList: [],
    },
    {
      name: 'Tom',
      age: 20,
      enjoyFoods: ['chicken', 'cake'],
      friendList: [],
    }
  ],
}

接口的继承

接口还可以继承,比如要对用户设置管理员,管理员信息也是一个对象,但要比普通用户多一个权限级别的属性,那么就可以使用继承,它通过 extends 来实现:

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
interface UserItem {
  name: string
  age: number
  enjoyFoods: string[]
  friendList: UserItem[]
}

// 这里继承了 UserItem 的所有属性类型,并追加了一个权限等级属性
interface Admin extends UserItem {
  permissionLevel: number
}

const admin: Admin = {
  name: 'Petter',
  age: 18,
  enjoyFoods: ['rice', 'noodle', 'pizza'],
  friendList: [
    {
      name: 'Marry',
      age: 16,
      enjoyFoods: ['pizza', 'ice cream'],
      friendList: [],
    },
    {
      name: 'Tom',
      age: 20,
      enjoyFoods: ['chicken', 'cake'],
      friendList: [],
    }
  ],
  permissionLevel: 1,
}

如果觉得继承的类型不需要记录这么多属性,也可以在继承的过程中舍弃某些属性,通过 Omit 帮助类型来实现,Omit 的类型如下;其中 T 代表已有的一个对象类型, K 代表要删除的属性名,如果只有一个属性就直接是一个字符串,如果有多个属性,用 | 来分隔开,下面的例子就是删除了两个不需要的属性:

1
type Omit<T, K extends string | number | symbol>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface UserItem {
  name: string
  age: number
  enjoyFoods: string[]
  friendList?: UserItem[]
}

// 这里在继承 UserItem 类型的时候,删除了两个多余的属性
interface Admin extends Omit<UserItem, 'enjoyFoods' | 'friendList'> {
  permissionLevel: number
}

// 现在的 admin 就非常精简了
const admin: Admin = {
  name: 'Petter',
  age: 18,
  permissionLevel: 1,
}

类是 JavaScript ES6 推出的一个概念,通过 class 关键字,可以定义一个对象的模板;在 TypeScript ,通过类得到的变量,它的类型就是这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个类
class User {
  // constructor 上的数据需要先这样定好类型
  name: string  // 成员属性,构造函数前先声明类成员的类型,否则报错

  // 入参也要定义类型
  // 构造函数,接收参数 userName,类型为 string,参数赋给当前对象 name 属性
  constructor(userName: string) {
    this.name = userName
  }

  getName() {
    console.log(this.name)
  }
}

// 通过 new 这个类得到的变量,它的类型就是这个类
const petter: User = new User('Petter')
petter.getName() // Petter

类与类之间可以继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这是一个基础类
class UserBase {
  name: string                    
  
  constructor(userName: string) { 
    this.name = userName
  }
}

// 这是另外一个类,继承自基础类
class User extends UserBase {
  getName() {
    console.log(this.name)
  }
}

// 这个变量拥有上面两个类的所有属性和方法
const petter: User = new User('Petter')
petter.getName()

类也可以提供给接口去继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这是一个类
class UserBase {
  name: string
  constructor(userName: string) {
    this.name = userName
  }
}

// 这是一个接口,可以继承自类
interface User extends UserBase {
  age: number
}

// 这样这个变量就必须同时存在两个属性
const petter: User = {
  name: 'Petter',
  age: 18,
}

如果类上面本身有方法存在,接口在继承的时候也要相应的实现,当然也可以借助在对象(接口)提到的 Omit 帮助类型来去掉这些方法。

联合类型

一个变量可能出现多种类型的值的时候,可以使用联合类型来定义它,类型之间用 符号分隔。例如:
1
2
3
4
5
6
7
8
// 可以在 demo 里运行这段代码
function counter(count: number | string) {
  console.log(`The current count is: ${count}.`)
}

// 不论传数值还是字符串,都可以达到的目的
counter(1)  // The current count is: 1.
counter('2')  // The current count is: 2.

函数

函数的基本的写法

在 JavaScript ,函数有很多种写法;但其实离不开两个最核心的操作——输入与输出,也就是对应函数的 “入参” 和 “返回值” 。在 TypeScript ,函数本身和 TS 类型有关系的也是在这两个地方。JS和TS的函数写法分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// JavaScript 代码

// 写法一:函数声明
function sum1(x, y) {
  return x + y
}

// 写法二:函数表达式
const sum2 = function (x, y) {
  return x + y
}

// 写法三:箭头函数
const sum3 = (x, y) => x + y

// 写法四:对象上的方法
const obj = {
  sum4(x, y) {
    return x + y
  },
}

// 还有很多……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// TypeScript 代码

// 写法一:函数声明
function sum1(x: number, y: number): number {
  return x + y
}

// 写法二:函数表达式
const sum2 = function(x: number, y: number): number {
  return x + y
}

// 写法三:箭头函数
const sum3 = (x: number, y: number): number => x + y

// 写法四:对象上的方法
const obj = {
  sum4(x: number, y: number): number {
    return x + y
  }
}

// 还有很多……

函数的可选参数

实际业务中会遇到有一些函数入参是可选,可以用和对象(接口)一样,用 ? 来定义:

1
2
3
4
5
6
7
8
// 注意 isDouble 这个入参后面有个 ? 号,表示可选
function sum(x: number, y: number, isDouble?: boolean): number {
  return isDouble ? (x + y) * 2 : x + y
}

// 这样传参都不会报错,因为第三个参数是可选的
sum(1, 2) // 3
sum(1, 2, true) // 6

无返回值的函数

除了有返回值的函数,更多时候是不带返回值的,这种函数用 void 来定义它的返回,也就是空:

1
2
3
4
5
6
// 注意这里的返回值类型
function sayHi(name: string): void {
  console.log(`Hi, ${name}!`)
}

sayHi('Petter') // Hi, Petter!

需要注意的是, voidnullundefined 不可以混用,如果的函数返回值类型是 null ,那么是真的需要 return 一个 null 值:

1
2
3
4
5
// 只有返回 null 值才能定义返回类型为 null
function sayHi(name: string): null {
  console.log(`Hi, ${name}!`)
  return null
}

有时候要判断参数是否合法,不符合要求时需要提前终止执行(比如在做一些表单校验的时候),这种情况下也可以用 void

1
2
3
4
5
6
7
function sayHi(name: string): void {
  // 这里判断参数不符合要求则提前终止运行,但它没有返回值
  if (!name) return

  // 否则正常运行
  console.log(`Hi, ${name}!`)
}

异步函数的返回值

对于异步函数,需要用 Promise<T> 类型来定义它的返回值,这里的 T 是泛型,取决于该函数最终返回一个什么样的值( async / await 也适用这个类型)。

例如这个例子,这是一个异步函数,会 resolve 一个字符串,所以它的返回类型是 Promise<string> (假如没有 resolve 数据,那么就是 Promise<void> ):

1
2
3
4
5
6
7
8
9
10
// 注意这里的返回值类型
function queryData(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Hello World')
    }, 3000)
  })
}

queryData().then((data) => console.log(data))

函数本身的类型

在 JS/TS 中,函数不仅是映射,而且是一种对象类型;函数可以被赋值、传递、作为参数、返回值,这就要求它有清晰的类型。例如:

1
2
3
4
// 隐式表明函数类型,因为TS会自动推导
const sum = (x: number, y: number): number => x + y
// 显式表明函数类型,明确这个变量是个“可调用对象”,接受两个number参数,返回一个number
const sum: (x: number, y: number) => number = (x: number, y: number): number => x + y

在某些情况下,显式写出函数类型是非常重要的:

  • 函数作为参数传递时

    1
    2
    3
    
      function handle(fn: (msg: string) => void) {
      fn('hello')
      }
    
  • 函数作为返回值时

    1
    2
    3
    
      function createHandler(): () => void {
      return () => console.log('hello')
      }
    
  • 函数作为对象方法时

    1
    2
    3
    4
    
      interface User {
      name: string;
      greet: (msg: string) => void;
      }
    
  • 回调函数、事件处理、泛型高阶函数:越复杂的函数逻辑,越依赖明确的函数签名,否则类型系统就“无能为力”了。

函数的重载

利用 TypeScript 的函数重载非常有用,会使得代码书写更清晰、也减少了使用TS类型断言的负担:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这一次用了函数重载
function greet(name: string): string  // TS 类型
function greet(name: string[]): string[]  // TS 类型
function greet(name: string | string[]) {
  if (Array.isArray(name)) {
    return name.map((n) => `Welcome, ${n}!`)
  }
  return `Welcome, ${name}!`
}

// 单个问候语,此时只有一个类型 string
const greeting = greet('Petter')
console.log(greeting) // Welcome, Petter!

// 多个问候语,此时只有一个类型 string[]
const greetings = greet(['Petter', 'Tom', 'Jimmy'])
console.log(greetings)
// [ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

任意值

如果实在不知道应该如何定义一个变量的类型, TypeScript 也允许使用任意值,通过“显式” 方式指定。为了程序能够正常运行,还要提高一下函数体内的代码健壮性。例如:

1
2
3
4
5
6
7
8
9
// 这里的入参显式指定了 any
function getFirstWord(msg: any) {
  // 这里使用了 String 来避免程序报错
  console.log(String(msg).split(' ')[0])
}

getFirstWord('Hello World')

getFirstWord(123)

npm包

存在一些包没有默认支持 TypeScript,此时只需要在原有的包名前面拼接@types,然后将其安装到 package.json 的 devDependencies 里即可解决该问题。

类型断言

当一个变量应用了联合类型时,在某些时候如果不显式的指明其中的一种类型,可能会导致后续的代码运行报错。这个时候就可以通过类型断言强制指定其中一种类型,以便程序顺利运行下去——即默认变量属于该种类型,而不再检查其合理性和是否完整。这样做的经典正例和反例如下:

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 greet(name: string | string[]): string | string[] {
  if (Array.isArray(name)) {
    return name.map((n) => `Welcome, ${n}!`)
  }
  return `Welcome, ${name}!`
}

// 已知此时应该是 string[] ,所以用类型断言将其指定为 string[]
const greetings = greet(['Petter', 'Tom', 'Jimmy']) as string[]

// 现在可以正常使用 join 方法
const greetingSentence = greetings.join(' ')
console.log(greetingSentence)

// 反例
// 原本要求 age 也是必须的属性之一
interface User {
  name: string
  age: number
}

// 但是类型断言过程中,遗漏了
const petter = {} as User
petter.name = 'Petter'

// TypeScript 依然可以运行下去,但实际上的数据是不完整的
console.log(petter) // { name: 'Petter' }

类型推论

类型推论的前提是变量在声明时有明确的值,如果一开始没有赋值,那么会被默认为 any 类型。

编译TypeScript为JavaScript

编译单个文件

先在 package.json 里增加一个 build script :

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
{
  "name": "hello-node",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev:cjs": "node src/cjs/index.cjs",
    "dev:esm": "node src/esm/index.mjs",
    "dev:ts": "ts-node src/ts/index.ts",
    "build": "tsc src/ts/index.ts --skipLibCheck --outDir dist",
    "compile": "babel src/babel --out-dir compiled",
    "serve": "node server/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "md5": "^2.3.0"
  },
  "devDependencies": {
    "@types/md5": "^2.3.2",
    "ts-node": "^10.7.0",
    "typescript": "^4.6.3"
  }
}

这样在命令行运行 npm run build 的时候,就会把 src/ts/index.ts 这个 TS 文件编译,并输出到项目下与 src 文件夹同级的 dist 目录下。

其中 tsc 是 TypeScript 用来编译文件的命令, --outDir 是它的一个选项,用来指定输出目录,如果不指定,则默认生成到源文件所在的目录下面。

例如对于src/ts/index.ts,可以先执行 npm run dev:ts 测试它的可运行性;然后编译它,在命令行输入 npm run build 并回车执行;可以看到多了一个 dist 文件夹,里面多了一个 index.js 文件;然后在命令行执行 node dist/index.js ,像之前测试 JS 文件一样使用 node 命令,运行 dist 目录下的 index.js 文件,它可以正确运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
hello-node
│ # 构建产物
├─dist
│ │ # 编译后的 JS 文件
│ └─index.js
│ # 依赖文件夹
├─node_modules
│ # 源码文件夹
├─src
│ # 锁定安装依赖的版本号
├─package-lock.json
│ # 项目清单
└─package.json

编译多个模块

刚才只是编译一个 index.ts 文件,如果 index.ts 里引入了其他模块,此时 index.ts 是作为入口文件,入口文件 import 进来使用的模块也会被 TypeScript 一并编译。

package.json 里的 build script 无需修改,依然只编译 index.ts ,但因为导入了 greet.ts ,所以 TypeScript 也会一并编译,来试一下运行 npm run build , 现在 dist 目录下有两个文件了:

1
2
3
4
5
6
7
8
hello-node
│ # 构建产物
├─dist
│ ├─greet.js  # 多了这个文件
│ └─index.js
│
│ # 其他文件这里省略...
└─package.json

在命令行执行 node dist/index.js ,虽然也是运行 dist 目录下的 index.js 文件,但这次它的作用是充当一个入口文件,引用到的 greet.js 模块文件也会被调用。这次一样可以得到正确的结果:

1
2
3
node dist/index.js
Welcome, Petter!
[ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

修改编译后的 JS 版本

可以修改编译配置,让 TypeScript 编译成不同的 JavaScript 版本。修改 package.json 里的 build script ,在原有的命令后面增加一个 --target 选项,--target 选项的作用是控制编译后的 JavaScript 版本。通常还需要配置一个 --module 选项,用于决定编译后是 CJS 规范还是 ESM 规范,但如果缺省,会根据 --target 来决定:

1
2
3
4
5
{
  "scripts": {
    "build": "tsc src/ts/index.ts --skipLibCheck --outDir dist --target es6"
  }
}

其它注意点

来到 修改编译后的JS版本,事情就开始变得复杂,编译的选项和测试成本都增加了很多。因此在实际的项目开发中,需要借助构建工具来处理很多编译过程中的兼容性问题,降低开发成本。

而刚才用到的诸如 --target 这样的选项,可以用一个更简单的方式来管理,类似于 package.json 项目清单, TypeScript 也有一份适用于项目的配置清单,请看 了解 tsconfig.json 部分。

了解 tsconfig.json

TypeScript 项目一般都会有一个 tsconfig.json 文件,放置于项目的根目录下,这个文件的作用是用来管理 TypeScript 在编译过程中的一些选项配置。在开始之前,需要全局安装一下 TypeScript :npm install -g typescript

这样可以使用 TypeScript 提供的全局功能,直接在命令行里使用 tsc 命令(之前本地安装的时候,需要封装成 package.json 的 script 才能调用它)。

在命令行输入 tsc --init ,这是 TypeScript 提供的初始化功能,会生成一个默认的 tsconfig.json 文件。现在的目录结构会多一个 tsconfig.json 文件。

每一个 tsc 的命令行的选项,都可以作为这个 JSON 的一个字段来管理,例如刚才的 --outDir--target 选项,可以直接在生成的 tsconfig.json 上面修改;在这个 JSON 文件里对应的就是:

1
2
3
4
5
6
7
{
  "compilerOptions": {
    "target": "es6",
    "module": "es6",
    "outDir": "./dist"
  }
}

这一次不需要用到 package.json 里的 build script 了,直接在命令行运行 tsc ,它现在会根据配置的 tsconfig.json 文件,按照的要求来编译。可以看到它依然按照要求在 dist 目录下生成编译后的 JS 文件,而且这一次的编译结果,和在 build script 里使用的 tsc src/ts/index.ts --outDir dist --target es6 这一长串命令是一样的。

所以正常工作中,都是使用 tsconfig.json 来管理 TypeScript 的配置的。

不过实际工作中的项目都是通过一些脚手架创建的,例如 Vue CLI ,或者现在的 Create Vue 或者 Create Preset ,都会在创建项目模板的时候,提前配置好通用的选项,只需要在不满足条件的情况下去调整。

本文由作者按照 CC BY 4.0 进行授权