快速上手 TypeScript

什么是TypeScript

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

虽然带有类型系统的前端语言不止 TypeScript (例如 Facebook 推出的 Flow.js ),但从目前整个 开源社区的流行趋势 看, TypeScript 无疑是更好的选择。
快速上手 TypeScript_第1张图片

而且只要本身已经学会了 JS ,并且经历过很多协作类的项目,那么使用 TS 编程是一个很自然而然的过程。

为什么要学TypeScript

要想知道自己为什么要用 TypeScript ,得先从 JavaScript 有什么不足说起,举一个非常小的例子:

function getFirstWord(msg) {
  console.log(msg.split(' ')[0])
}

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

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

这里定义了一个用空格切割字符串的方法,并打印出第一个单词:

  • 第一次执行时,字符串支持 split 方法,所以成功获取到了第一个单词 Hello
  • 第二次执行时,由于数值不存在 split 方法,所以传入 123 引起了程序崩溃
    这就是 JavaScript 的弊端,过于灵活,没有类型的约束,很容易因为类型的变化导致一些本可避免的 BUG 出现,而且这些 BUG 通常需要在程序运行的时候才会被发现,很容易引发生产事故。

虽然可以在执行 split 方法之前执行一层判断或者转换,但很明显增加了很多工作量。

TypeScript 的出现,在编译的时候就可以执行检查来避免掉这些问题,而且配合 VSCode 等编辑器的智能提示,可以很方便的知道每个变量对应的类型。

学习 TypeScript 是一个明智的选择,特别是在使用 Vue 3 进行开发时。以下是一些原因:

  1. 强类型系统:TypeScript 是 JavaScript 的超集,它引入了静态类型检查。通过在代码中添加类型注解,可以在开发过程中捕获许多常见的错误,并提供更好的代码提示和自动补全。这有助于提高代码的可靠性和可维护性。

  2. Vue 3 的完善支持:Vue 3 是为 TypeScript 设计的,并且提供了完整的类型定义。使用 TypeScript 可以充分发挥 Vue 3 的类型推断和类型检查功能,提供更好的开发体验和代码质量。

  3. 市场需求和就业机会:TypeScript 在前端开发领域的使用越来越广泛。许多公司和项目都在使用 TypeScript 进行开发,并且对具备 TypeScript 技能的开发者有很高的需求。学习 TypeScript 可以增加你的就业机会,并使你在竞争激烈的市场中更具竞争力。

  4. 社区支持和工具生态系统:TypeScript 拥有庞大的开发者社区和丰富的工具生态系统。你可以轻松地找到大量的学习资源、文档和库,以及与 TypeScript 集成的开发工具和编辑器插件。

Hello TypeScript

将继续使用 Hello Node 这个 demo ,或者可以再建一个新 demo ,依然是在 src 文件夹下,创建一个 ts 文件夹归类本次的测试文件,然后创建一个 index.ts 文件在 ts 文件夹下。

TypeScript 语言对应的文件扩展名是 .ts 。

然后在命令行通过 cd 命令进入项目所在的目录路径,安装 TypeScript 开发的两个主要依赖包:

  • typescript 这个包是用 TypeScript 编程的语言依赖包

  • ts-node 是让 Node 可以运行 TypeScript 的执行环境

npm install -D typescript ts-node

这次添加了一个 -D 参数,因为 TypeScript 和 TS-Node 是开发过程中使用的依赖,所以将其添加到 package.json 的 devDependencies 字段里。

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

{
  "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",
    "compile": "babel src/babel --out-dir compiled",
    "serve": "node server/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "md5": "^2.3.0"
  },
  "devDependencies": {
    "ts-node": "^10.7.0",
    "typescript": "^4.6.3"
  }
}

准备工作完毕!

请注意, dev:ts 这个 script 是用了 ts-node 来代替原来在用的 node ,因为使用 node 无法识别 TypeScript 语言。

把 为什么需要类型系统 里面提到的例子放到 src/ts/index.ts 里:

// src/ts/index.ts
function getFirstWord(msg) {
  console.log(msg.split(' ')[0])
}

getFirstWord('Hello World')

getFirstWord(123)

然后在命令行运行 npm run dev:ts 来看看这次的结果:

TSError: ⨯ Unable to compile TypeScript:
src/ts/index.ts:1:23 - error TS7006: Parameter 'msg' implicitly has an 'any' type.

1 function getFirstWord(msg) {
                        ~~~

这是告知 getFirstWord 的入参 msg 带有隐式 any 类型,这个时候可能还不了解 any 代表什么意思,没关系,来看下如何修正这段代码:

// src/ts/index.ts
function getFirstWord(msg: string) {
  console.log(msg.split(' ')[0])
}

getFirstWord('Hello World')

getFirstWord(123)

留意到没有,现在函数的入参 msg 已经变成了 msg: string ,这是 TypeScript 指定参数为字符串类型的一个写法。

现在再运行 npm run dev:ts ,上一个错误提示已经不再出现,取而代之的是一个新的报错:

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)
               ~~~

这次的报错代码是在 getFirstWord(123) 这里,告诉 number 类型的数据不能分配给 string 类型的参数,也就是故意传入一个会报错的数值进去,被 TypeScript 检查出来了!

可以再仔细留意一下控制台的信息,会发现没有报错的 getFirstWord(‘Hello World’) 也没有打印出结果,这是因为 TypeScript 需要先被编译成 JavaScript ,然后再执行。

这个机制让有问题的代码能够被及早发现,一旦代码出现问题,编译阶段就会失败。

移除会报错的那行代码,只保留如下:

 src/ts/index.ts
function getFirstWord(msg: string) {
  console.log(msg.split(' ')[0])
}

getFirstWord('Hello World')

再次运行 npm run dev:ts ,这次完美运行!

npm run dev:ts

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

Hello

在这个例子里,相信已经感受到 TypeScript 的魅力了!接下来来认识一下不同的 JavaScript 类型,在 TypeScript 里面应该如何定义。

常用的 TS 类型定义

原始数据类型

原始数据类型 是一种既非对象也无方法的数据,刚才演示代码里,函数的入参使用的字符串 String 就是原始数据类型之一。

除了 String ,另外还有数值 Number 、布尔值 Boolean 等等,它们在 TypeScript 都有统一的表达方式,列个表格对比,能够更直观的了解:

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

有没有发现窍门?对! TypeScript 对原始数据的类型定义真的是超级简单,就是转为全小写即可!

举几个例子:

// 字符串
const str: string = 'Hello World'

// 数值
const num: number = 1

// 布尔值
const bool: boolean = true

不过在实际的编程过程中,原始数据类型的类型定义是可以省略的,因为 TypeScript 会根据声明变量时赋值的类型,自动推导变量类型,也就是可以跟平时写 JavaScript 一样:

// 这样也不会报错,因为 TS 会推导它们的类型
const str = 'Hello World'
const num = 1
const bool = true

数组

除了原始数据类型之外, JavaScript 还有引用类型,数组 Array 就是其中的一种。

之所以先讲数组,是因为它在 TS 类型定义的写法上面,可能是最接近原始数据的一个类型了,为什么这么说?还是列个表格,来看一下如何定义数组:

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

是吧!就只是在原始数据类型的基础上变化了一下书写格式,就成为了数组的定义!

笔者最常用的就是 string[] 这样的格式,只需要追加一个方括号 [] ,另外一种写法是基于 TS 的泛型 Array ,两种方式定义出来的类型其实是一样的。

举几个例子:

// 字符串数组
const strs: string[] = ['Hello World', 'Hi World']

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

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

在实际的编程过程中,如果的数组一开始就有初始数据(数组长度不为 0 ),那么 TypeScript 也会根据数组里面的项目类型,正确自动帮推导这个数组的类型,这种情况下也可以省略类型定义:

// 这种有初始项目的数组, TS 也会推导它们的类型
const strs = ['Hello World', 'Hi World']
const nums = [1, 2, 3]
const bools = [true, true, false]

但是!如果一开始是 [] ,那么就必须显式的指定数组类型(取决于当前项目的 tsconfig.json 配置,可能会引起报错):

ts

// 这个时候会认为是 any[] 或者 never[] 类型
const nums = []

// 这个时候再 push 一个 number 数据进去,也不会使其成为 number[]
nums.push(1)

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

对象(接口)

看完了数组,接下来看看对象的用法,对象也是引用类型,在 数组 的最后提到了一个 UserItem[] 的写法,这里的 UserItem 就是一个对象的类型定义。

如果熟悉 JavaScript ,那么就知道对象的 “键值对” 里面的值,可能是由原始数据、数组、对象组成的,所以在 TypeScript ,类型定义也是需要根据值的类型来确定它的类型,因此定义对象的类型应该是第一个比较有门槛的地方。

如何定义对象的类型

对象的类型定义有两个语法支持: typeinterface

先看看 type 的写法:

type UserItem = {
  // ...
}

再看看 interface 的写法:

interface UserItem {
  // ...
}

可以看到它们表面上的区别是一个有 = 号,一个没有,事实上在一般的情况下也确实如此,两者非常接近,但是在特殊的时候也有一定的区别。

了解接口的使用

为了降低学习门槛,统一使用 interface 来做入门教学,它的写法与 Object 更为接近,事实上它也被用的更多。

对象的类型 interface 也叫做接口,用来描述对象的结构。

TIP

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

这里通过一些举例来带举一反三,随时可以在 demo 里进行代码实践。

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

// 定义用户对象的类型
interface UserItem {
  name: string
  age: number
}

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

如果需要添加数组、对象等类型到属性里,按照这样继续追加即可。

可选的接口属性

注意,上面这样定义的接口类型,表示 nameage 都是必选的属性,不可以缺少,一旦缺少,代码运行起来就会报错!

src/ts/index.ts 里敲入以下代码,也就是在声明变量的时候故意缺少了 age 属性,来看看会发生什么:

ts

// 注意!这是一段会报错的代码

interface UserItem {
  name: string
  age: number
}

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

运行 npm run dev:ts ,会看到控制台给的报错信息,缺少了必选的属性 age

src/ts/index.ts:6:7 - error TS2741:
Property 'age' is missing in type '{ name: string; }'
but required in type 'UserItem'.

6 const petter: UserItem = {
        ~~~~~~

  src/ts/index.ts:3:3
    3   age: number
        ~~~
    'age' is declared here.

在实际的业务中,有可能会出现一些属性并不是必须的,就像这个年龄,可以将其设置为可选属性,通过添加 ? 来定义。

请注意下面代码的第三行, age 后面紧跟了一个 ? 号再接 : 号,这是 TypeScript 对象对于可选属性的一个定义方式,这一次这段代码是可以成功运行的!

ts

interface UserItem {
  name: string
  // 这个属性变成了可选
  age?: number
}

const petter: UserItem = {
  name: 'Petter',
}
调用自身接口的属性

如果一些属性的结构跟本身一致,也可以直接引用,比如下面例子里的 friendList 属性,用户的好友列表,它就可以继续使用 UserItem 这个接口作为数组的类型:

ts

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 来实现:

ts

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,
}

如果觉得这个 Admin 类型不需要记录这么多属性,也可以在继承的过程中舍弃某些属性,通过 Omit 帮助类型来实现,Omit 的类型如下:

ts

type Omit

其中 T 代表已有的一个对象类型, K 代表要删除的属性名,如果只有一个属性就直接是一个字符串,如果有多个属性,用 | 来分隔开,下面的例子就是删除了两个不需要的属性:

ts

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 关键字,可以定义一个对象的模板,如果对类还比较陌生的话,可以先阅读一下阮一峰老师的 ES6 文章:Class 的基本语法 。

在 TypeScript ,通过类得到的变量,它的类型就是这个类,可能这句话看起来有点难以理解,来看个例子,可以在 demo 里运行它:

// 定义一个类
class User {
  // constructor 上的数据需要先这样定好类型
  name: string

  // 入参也要定义类型
  constructor(userName: string) {
    this.name = userName
  }

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

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

类与类之间可以继承:

// 这是一个基础类
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()

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

// 这是一个类
class UserBase {
  name: string
  constructor(userName: string) {
    this.name = userName
  }
}

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

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

如果类上面本身有方法存在,接口在继承的时候也要相应的实现

class UserBase {
  name: string
  constructor(userName: string) {
    this.name = userName
  }
  // 这是一个方法
  getName() {
    console.log(this.name)
  }
}

// 接口继承类的时候也可以去掉类上面的方法
interface User extends Omit<UserBase, 'getName'> {
  age: number
}

// 最终只保留数据属性,不带有方法
const petter: User = {
  name: 'Petter',
  age: 18,
}

联合类型

阅读到这里,对 JavaScript 的数据和对象如何在 TypeScript 定义类型相信没有太大问题了吧!

当一个变量可能出现多种类型的值的时候,可以使用联合类型来定义它,类型之间用 | 符号分隔。

举一个简单的例子,下面这个函数接收一个代表 “计数” 的入参,并拼接成一句话打印到控制台,因为最终打印出来的句子是字符串,所以参数没有必要非得是数值,传字符串也是可以的,所以就可以使用联合类型:

// 可以在 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.

注意: 在上面 counter 函数的 console.log 语句里,使用了一个 ````符号来定义字符串,这是 ES6 语法里的 模板字符串 ,它和传统的单引号 / 双引号相比更为灵活,特别是遇到字符串需要配合多变量拼接和换行的情况。

对 JavaScript 后面推出的新语法不太熟悉的话,很容易和单引号混淆,在学名上,它也被称之为 “反引号” ( Backquote ) ,可以使用标准键盘的 ESC 键下方、也就是 1 左边的那个按键打出来。

在实际的业务场景中,例如 Vue 的路由在不同的数据结构里也有不同的类型,有时候需要通过路由实例来判断是否符合要求的页面,也需要用到这种联合类型:

// 注意:这不是完整的代码,只是一个使用场景示例
import type { RouteRecordRaw, RouteLocationNormalizedLoaded } from 'vue-router'

function isArticle(
  route: RouteRecordRaw | RouteLocationNormalizedLoaded
): boolean {
  // ...
}

再举个例子,是用 Vue 做页面,会涉及到子组件或者 DOM 的操作,当它们还没有渲染出来时,获取到的是 null ,渲染后才能拿到组件或者 DOM 结构,这种场景也可以使用联合类型:

// querySelector 拿不到 DOM 的时候返回 null
const ele: HTMLElement | null = document.querySelector('.main')

当决定使用联合类型的时候,大部分情况下可能需要对变量做一些类型判断再写逻辑,当然有时候也可以无所谓,就像第一个例子拼接字符串那样。

这一小节在这里做简单了解即可,因为下面会继续配合不同的知识点把这个联合类型再次拿出来讲。

函数

函数是 JavaScript 里最重要的成员之一,所有的功能实现都是基于函数。

函数的基本的写法

在 JavaScript ,函数有很多种写法:

// 注意:这是 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
  },
}

// 还有很多……

但其实离不开两个最核心的操作:输入与输出,也就是对应函数的 “入参” 和 “返回值” ,在 TypeScript ,函数本身和 TS 类型有关系的也是在这两个地方。

函数的入参是把类型写在参数后面,返回值是写在圆括号后面,把上面在 JavaScript 的这几个写法,转换成 TypeScript 看看区别在哪里:

// 注意:这是 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
  }
}

// 还有很多……

是不是一下子 Get 到了技巧!函数的类型定义也是非常的简单,掌握这个技巧可以让解决大部分常见的函数。

函数的可选参数

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

// 注意 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 来定义它的返回,也就是空。

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

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

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

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

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

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

  // 否则正常运行
  console.log(`Hi, ${name}!`)
}
异步函数的返回值

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

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

// 注意这里的返回值类型
function queryData(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Hello World')
    }, 3000)
  })
}

queryData().then((data) => console.log(data))
函数本身的类型

细心的开发者可能会有个疑问,通过函数表达式或者箭头函数声明的函数,这样写好像只对函数体的类型进行了定义,而左边的变量并没有指定。

没错,确实是没有为这个变量指定类型:

// 这里的 sum ,确实是没有指定类型
const sum = (x: number, y: number): number => x + y

这是因为,通常 TypeScript 会根据函数体自动推导,所以可以省略这里的定义。

如果确实有必要,可以这样来定义等号左边的类型:

const sum: (x: number, y: number) => number = (x: number, y: number): number =>
  x + y

这里出现了 2 个箭头 => ,注意第一个箭头是 TypeScript 的,第二个箭头是 JavaScript ES6 的。

实际上上面这句代码是分成了三部分:

  1. const sum: (x: number, y: number) => number 是这个函数的名称和类型
  2. = (x: number, y: number) 这里是指明了函数的入参和类型
  3. : number => x + y 这里是函数的返回值和类型

第 2 和 3 点相信从上面的例子已经能够理解了,所以注意力放在第一点:

TypeScript 的函数类型是以 () => void 这样的形式来写的:左侧圆括号是函数的入参类型,如果没有参数,就只有一个圆括号,如果有参数,就按照参数的类型写进去;右侧则是函数的返回值。

事实上由于 TypeScript 会推导函数类型,所以很少会显式的去写出来,除非在给对象定义方法:

// 对象的接口
interface Obj {
  // 上面的方法就需要显式的定义出来
  sum: (x: number, y: number) => number
}

// 声明一个对象
const obj: Obj = {
  sum(x: number, y: number): number {
    return x + y
  }
}
函数的重载

在未来的实际开发中,可能会接触到一个 API 有多个 TS 类型的情况,比如 Vue 的 watch API。

Vue 的这个 watch API 在被调用时,需要接收一个数据源参数,当侦听单个数据源时,它匹配了类型 1 ,当传入一个数组侦听多个数据源时,它匹配了类型 2 。

这个知识点其实就是 TypeScript 里的函数重载。

先来看一下在不使用函数重载时应该如何编写代码:

ts

// 对单人或者多人打招呼
function greet(name: string | string[]): string | string[] {
  if (Array.isArray(name)) {
    return name.map((n) => `Welcome, ${n}!`)
  }
  return `Welcome, ${name}!`
}

// 单个问候语
const greeting = greet('Petter')
console.log(greeting) // Welcome, Petter!

// 多个问候语
const greetings = greet(['Petter', 'Tom', 'Jimmy'])
console.log(greetings)
// [ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

需要注意的是:这里的入参和返回值使用了 TypeScript 联合类型的 ,不了解的话请先重温知识点。

虽然代码逻辑部分还是比较清晰的,区分了入参的数组类型和字符串类型,返回不同的结果,但是,在入参和返回值的类型这里,却显得非常乱。

并且这样子写,下面在调用函数时,定义的变量也无法准确的获得它们的类型:

// 此时这个变量依然可能有多个类型
const greeting: string | string[]

如果要强制确认类型,需要使用 TS 的类型断言 (留意后面的 as 关键字):

const greeting = greet('Petter') as string
const greetings = greet(['Petter', 'Tom', 'Jimmy']) as string[]

这无形的增加了编码时的心智负担。

此时,利用 TypeScript 的函数重载就非常有用!来看一下具体如何实现:

// 这一次用了函数重载
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!' ]

上面是利用函数重载优化后的代码,可以看到一共写了 3 行 function greet … ,区别如下:

第 1 行是函数的 TS 类型,告知 TypeScript ,当入参为 string 类型时,返回值也是 string ;

第 2 行也是函数的 TS 类型,告知 TypeScript ,当入参为 string[] 类型时,返回值也是 string[] ;

第 3 行开始才是真正的函数体,这里的函数入参需要把可能涉及到的类型都写出来,用以匹配前两行的类型,并且这种情况下,函数的返回值类型可以省略,因为在第 1 、 2 行里已经定义过返回类型了。

任意值

如果实在不知道应该如何定义一个变量的类型, TypeScript 也允许使用任意值。

src/ts/index.ts

// 这段代码在 TS 里运行会报错
function getFirstWord(msg) {
  console.log(msg.split(' ')[0])
}

getFirstWord('Hello World')

getFirstWord(123)

运行 npm run dev:ts 的时候,会得到一句报错 Parameter 'msg' implicitly has an 'any' type. ,意思是这个参数带有隐式 any 类型。

这里的 any 类型,就是 TypeScript 任意值。

既然报错是 “隐式” ,那 “显式” 的指定就可以了,当然,为了程序能够正常运行,还提高一下函数体内的代码健壮性:

ts

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

getFirstWord('Hello World')

getFirstWord(123)

这次就不会报错了,不论是传 string 还是 number 还是其他类型,都可以正常运行。

TIP

使用 any 的目的是让在开发的过程中,可以不必在无法确认类型的地方消耗太多时间,不代表不需要注意代码的健壮性。

一旦使用了 any ,代码里的逻辑请务必考虑多种情况进行判断或者处理兼容。

npm 包

虽然现在从 npm 安装的包都基本自带 TS 类型了,不过也存在一些包没有默认支持 TypeScript ,比如前面提到的 md5 。

在 TS 文件里导入并使用这个包的时候,会编译失败,比如以下代码:

// src/ts/index.ts
import md5 from 'md5'
console.log(md5('Hello World'))

在命令行执行 npm run dev:ts 之后,会得到一段报错信息:

src/ts/index.ts:1:17 - error TS7016:
Could not find a declaration file for module 'md5'.
'D:/Project/demo/hello-node/node_modules/md5/md5.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/md5` if it exists
  or add a new declaration (.d.ts) file
  containing `declare module 'md5';`

1 import md5 from 'md5'
                  ~~~~~

这是因为缺少 md5 这个包的类型定义,根据命令行的提示,安装 @types/md5 这个包。

这是因为这些包是很早期用 JavaScript 编写的,因为功能够用作者也没有进行维护更新,所以缺少相应的 TS 类型,因此开源社区推出了一套 @types 类型包,专门处理这样的情况。

@types 类型包的命名格式为 @types/ ,也就是在原有的包名前面拼接 @types ,日常开发要用到的知名 npm 包都会有相应的类型包,只需要将其安装到 package.json 的 devDependencies 里即可解决该问题。

来安装一下 md5 的类型包:

npm install -D @types/md5

再次运行就不会报错了!

npm run dev:ts

> [email protected] dev:ts
> ts-node src/ts/index.ts

b10a8db164e0754105b7a99be72e3fe5

类型断言

在讲解 函数的重载 的时候,提到了一个用法:

const greeting = greet('Petter') as string

这里的 值 as 类型 就是 TypeScript 类型断言的语法,它还有另外一个语法是 <类型>值

当一个变量应用了联合类型 时,在某些时候如果不显式的指明其中的一种类型,可能会导致后续的代码运行报错。

这个时候就可以通过类型断言强制指定其中一种类型,以便程序顺利运行下去。

常见的使用场景

把函数重载时最开始用到的那个例子,也就是下面的代码放到 src/ts/index.ts 里:

// 对单人或者多人打招呼
function greet(name: string | string[]): string | string[] {
  if (Array.isArray(name)) {
    return name.map((n) => `Welcome, ${n}!`)
  }
  return `Welcome, ${name}!`
}

// 虽然已知此时应该是 string[]
// 但 TypeScript 还是会认为这是 string | string[]
const greetings = greet(['Petter', 'Tom', 'Jimmy'])

// 会导致无法使用 join 方法
const greetingSentence = greetings.join(' ')
console.log(greetingSentence)

执行 npm run dev:ts ,可以清楚的看到报错原因,因为 string 类型不具备 join 方法。

src/ts/index.ts:11:31 - error TS2339:
Property 'join' does not exist on type 'string | string[]'.
  Property 'join' does not exist on type 'string'.

11 const greetingStr = greetings.join(' ')
                                 ~~~~

此时利用类型断言就可以达到目的:

// 对单人或者多人打招呼
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' }

使用类型断言可以让 TypeScript 不再检查该代码,默认是正确无误的。

所以,请务必保证这段代码真的是正确的!

类型推论

在实际的编程过程中,原始数据类型的类型定义是可以省略的,因为 TypeScript 会根据声明变量时赋值的类型,自动帮推导变量类型

这其实是 TypeScript 的类型推论功能,当在声明变量的时候可以确认它的值,那么 TypeScript 也可以在这个时候帮推导它的类型,这种情况下就可以省略一些代码量。

下面这个变量这样声明是 OK 的,因为 TypeScript 会推导 msgstring 类型。

// 相当于 msg: string
let msg = 'Hello World'

// 所以要赋值为 number 类型时会报错
msg = 3 // Type 'number' is not assignable to type 'string'

下面这段代码也是可以正常运行的,因为 TypeScript 会根据 return 的结果推导 getRandomNumber 的返回值是 number 类型,从而推导变量 num 也是 number 类型。

// 相当于 getRandomNumber(): number
function getRandomNumber() {
  return Math.round(Math.random() * 10)
}

// 相当于 num: number
const num = getRandomNumber()

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

// 此时相当于 foo: any
let foo

// 所以可以任意改变类型
foo = 1 // 1
foo = true // true

类型推论可以节约很多书写工作量,在确保变量初始化有明确的值的时候,可以省略其类型,但必要的时候,该写上的还是要写上。

如何编译为 JavaScript 代码

学习到这里,对于 TypeScript 的入门知识已经学到了吧!

前面学习的时候,一直是基于 dev:ts 命令,它调用的是 ts-node 来运行的 TS 文件:

{
  // ...
  "scripts": {
    // ...
    "dev:ts": "ts-node src/ts/index.ts"
  }
  // ...
}

但最终可能需要的是一个 JS 文件,比如要通过

你可能感兴趣的:(vue3入门,typescript,javascript,前端)