Vue3——TypeScript基础

TypeScript基础

我们知道,JavaScript是弱类型的程序语言,而TypeScript是强类型的程序语言。对于弱类型的JavaScript来说,在编写代码时,正确语法编写的补全提示能力和错误语法编写的提示能力都非常"弱"​,非常不利于开发者的项目编码工作。而强类型的TypeScript具有非常友好的补全提示或错误提示能力,其可以极大地提高项目的编码效率,而且项目运行调试过程中的Bug也会减少很多。

1、TypeScript概述

1.1、TypeScript是什么

这需要从JavaScript说起,因为JavaScript是一门弱类型即时编译型的程序语言,在利用JavaScript进行变量声明时,它没有强制约束要实现的类型,这就使得在利用JavaScript开发项目时,更容易出现类型不对应的情况,从而导致后续操作更容易出现程序错误,并且不容易被发现。如果类似情况出现在团队化开发中,则会引起更多问题,从而影响项目的顺利进展。

为了弥补JavaScript弱类型的不足,TypeScript这一强类型的程序语言就诞生了。TypeScript弥补了JavaScript的不足,添加了更多的语法来支持类型,并且要求开发者在定义各种类型变量时明确类型,从而增强程序的健壮性,以更大程度降低程序出错的概率,实现项目开发速度的提升。不过浏览器等运行环境的解析并不直接支持TypeScript,最终需要通过编译转换工具将TypeScript编译成JavaScript,因此TypeScript是来源于JavaScript,并最终归结于JavaScript的,如图所示。

TypeScript被编译成JavaScript的过程TypeScript是JavaScript的超集,是建立在JavaScript之上的,我们可以将它理解为JavaScript的高级版,如图所示。

值得一提的是,TypeScript的文件后缀与JavaScript的不同,不再是".js"​,而是".ts"​。

1.2、TypeScript是谁开发的

TypeScript是由微软开发的。俗话说"背靠大树好乘凉"​,那么由微软开发的TypeScript自然是非常值得信赖与依靠的。JavaScript是目前世界排名前10的程序语言,它拥有异常庞大的用户群体,根据Stack Overflow的统计,JavaScript在2008年用户量排名进入前10后,一直稳步提升,目前是用户量最大的程序语言。TypeScript作为JavaScript的超集,JavaScript的拥护者也会非常容易地投入TypeScript的怀抱,因此TypeScript在TIOBE排行榜上一直稳步提升。

1.3、TypeScript是什么时间诞生的

When在这里的全称为"When was TypeScriptborn?​"​,翻译过来就是"TypeScript是什么时间诞生的?​"​。

2012年10月,微软发布了TypeScript的第1个版本(0.8), 到目前为止,开发者经过10多年的不懈努力,完善了TypeScript对前端Angular、React、Vue等众多框架的支持,也逐步强化了其对Node、Deno等后台运行环境的支撑。

其实在互联网这个优胜劣汰的环境中,任何一个项目或者一门语言能存活10年都不是一件容易的事情,与Java、Python不同,TypeScript没有经历30年的风雨,也不像JavaScript一样经受了20年的雷霆磨砺,但它依旧在10多年的挫折中不断强大,这样看来,TypeScript未来的发展将是一片光明。

1.4、TypeScript适用于什么样的项目

TypeScript适用于任何规模、任何类型的项目!

TypeScript更适用于中大型项目。对比JavaScript,TypeScript提供的数据类型可以为项目带来更高的可维护性,以及更少的Bug。

因为TypeScript来源于JavaScript又归结于JavaScript,所以原来基于JavaScript的项目,可以直接利用TypeScript进行改造。因为TypeScript完全可以和JavaScript共存,所以开发人员可以利用TypeScript升级强化JavaScript程序代码,将代码逐步替换为TypeScript的类型声明和约束,以增强程序的健壮性。

团队化开发的中大型项目如果直接利用TypeScript开发会更为有利。TypeScript可以明确定义各个变量的类型,在多人操作同一程序模块时根本不需要纠结或担心类型是否会出错,因为TypeScript有强制约束和智能代码提示,这样可以极大地减少类型不对应等初级错误。也正是因为标准的一致性,在团队化开发时还可以避免成员之间无意义的沟通,大幅度减少沟通成本。

1.5、为什么引用TypeScript

想要知道为什么引用TypeScript,就要清楚TypeScript的主要目标:利用强类型定义尽可能降低程序出错的概率,从而增强项目代码的可维护性,提高项目的开发效率。

下面分两点进行详细分析。

1. TypeScript可以提高开发者的工作效率,同时帮助开发者避免错误。

TypeScript提供的数据类型可以帮助开发者避免许多错误。通过使用TypeScript,开发者可以实现错误前置,在编写代码时就可以获知错误信息,而不是在运行时才发现错误。

下面使用JavaScript和TypeScript来实现同一个需求,读者可以从中感受TypeScript的优势。

需求为:封装一个函数,在函数内部将两个数字相加。例如,数字为x和y,返回的结果为"x+y"的和。

JavaScript代码实现如下。

js 复制代码
function add(x, y) {
    return x + y
}
js 复制代码
//正确调用如下。
const result = add(2, 3); //5
//错误调用如下
const result2 = add('2', '3'); //'23'

如何限制错误调用呢?TypeScript的语法如下。

ts 复制代码
function add(x: number, y: number): number {
    return x + y;
}

从上述代码中可以看出,在形参接口的部分将其接收形参的类型设置为了数值类型,这就代表该函数只能接收数值类型的数据,而不能接收任何其他类型的数据。在使用与上述相同方式调用函数时,可以发现TypeScript编译器会报错。TypeScript成功实现了错误前置,防止了程序在运行时发生错误,这就是TypeScript的类型约束,明显可以看出其代码功能性变强、可读性更高。需要注意的是,这里只是简单演示TypeScript实现的效果,具体语法在9.7节中会详细讲解。

调用add函数的代码如下。

js 复制代码
const result3 = add(2, 3); //5
const result4 = add('2', '3'); //直接提示错误

2. 与ECMAScript标准同步发展。

引用TypeScript的另一个重要原因就是它坚持与ECMAScript标准同步发展。TypeScript的发展紧跟ECMAScript历史发展的步伐,只要ECMAScript标准进入候选阶段,TypeScript就会尝试实现其功能,这就让企业和开发人员可以放心地使用TypeScript,而不需要担心是否会与ECMAScript标准产生冲突。

1.6、怎样学习TypeScript

TypeScript名字中的Type翻译为中文是"类型"的意思,说明"类型"是TypeScript最核心的特性,因此学好TypeScript的关键在于掌握TypeScript的类型。TypeScript除了支持JavaScript所有的基础和引用类型,还提供了特有的类型,主要包括tuple(元组)​、enum(枚举)​、any(任意)​、void(空)​、union(联合)​、interface(接口)​、generic(泛型)等。读者只要从TypeScript的基础类型出发,细化TypeScript中类型的规范、定义、使用等流程,就能够快速掌握TypeScript。

2、安装TypeScript环境

2.1、TypeScript程序不能直接运行

TypeScript程序无法在浏览器或者Node环境中直接运行,这一点可以通过实操来证实。先创建一个名为typescript-setup的空目录,然后在此目录下新建一个TypeScript程序文件helloWorld.ts,并编写一段简单的TypeScript代码,指定变量hi的类型为字符串类型。

helloWorld.ts文件代码如下。

ts 复制代码
const hi: string = '你好,这是TypeScript程序文件';
console.log(hi);

现在新建一个index.html网页,并在该网页中利用script标签引入helloWorld.ts文件,打开Vue开发者调试工具的控制台,则会看到错误内容显示,表明无法正常运行TypeScript程序文件,如图所示。

通过node命令运行代码是不是就不会报错了呢?下面尝试在Node环境中运行代码,命令如下。

js 复制代码
node helloWorld.ts

运行代码后发现,程序依旧报错,表明在Node环境下,TypeScript程序文件也无法直接正常运行,如图所示。

2.2、安装TypeScript环境并测试

在操作系统全局环境下安装TypeScript命令行工具,命令如下。

bash 复制代码
npm install -g typescript

执行上方命令后,系统会在全局环境下提供tsc命令。安装完成之后,我们就可以在任意环境下执行tsc命令将.ts文件编译为.js文件。

此时再来编译TypeScript程序文件只需执行下方命令即可。

bash 复制代码
tsc helloWorld.ts

此时在helloWorld.ts文件的同级目录下会产生一个helloWorld.js文件,代码内容已经被编译成下方的JavaScript代码。

js 复制代码
var hi = '你好,这是TypeScript程序文件';
console.log(hi);

那么在HTML中也不能再引入".ts"文件了,而是需要替换成编译以后的".js"文件,此时浏览器能够正常运行当前的程序。

在Node环境下测试".js"文件,运行的结果也是正常的,命令如下。

bash 复制代码
node helloWorld.js

因此,无论是在浏览器环境下,还是在Node环境下,TypeScript程序都是不能直接运行的。

3、一切从HelloWorld开始

3.1、在编译时对数据类型进行静态检查

新建一个名为typescript-hello的目录,并在其中新建文件hello.ts,将下方代码编写在hello.ts文件中。

ts 复制代码
function sayHello(person: string) {
    return 'Hello,' + person;
}

let user = 'Tom';
console.log(sayHello(user));

在终端中执行下方命令。

bash 复制代码
tsc hello.ts

此时生成的编译文件为hello.js,文件代码如下。

js 复制代码
"use strict";
function sayHello(person) {
    return 'Hello,' + person;
}
let user = 'Tom';
console.log(sayHello(user));

在TypeScript中,可以使用"​:​"指定变量的数据类型,值得一提的是,​"​:​"的前后可以包含空格。

在上述例子中,我们使用"​:​"指定person参数的数据类型为string,但在编译为JavaScript代码后,可以发现检查的代码并没有被插进来。这是因为TypeScript只会在编译时对数据类型进行静态检查,如果发现错误,在编译时就会报错。

下面修改user的类型,将原来的string类型修改为数组类型。

hello.ts文件代码如下。

这时候VSCode编辑器就会在出错的代码下方显示红色波浪线,将鼠标指针移动到上面会有对应的错误提示。其实不仅VSCode编辑器会提示错误,TypeScript在编译时也会报错。

尽管出现了报错,但系统依旧会生成对应的JavaScript文件,如下所示。

js 复制代码
"use strict";
function sayHello(person) {
    return 'Hello,' + person;
}
let user = [0, 1, 2];
console.log(sayHello(user));

虽然TypeScript在编译时报错了,但是在默认情况下,我们依旧可以使用这个编译之后的文件。

如果想要在报错时终止生成JavaScript文件,则可以在tsconfig.json文件中配置noEmitOnError属性。那么tsconfig.json文件的作用是什么呢?为什么要设置tsconfig.json文件呢?

3.2、tsconfig.json环境配置

现在修改hello.ts文件,首先在其中添加一个数组并传递给函数sayHello,然后查看编译后的hello.js文件,会发现hello.ts文件修改后生成的hello.js文件不会发生任何改变。如果每次修改hello.ts文件都需要通过tsc命令不断编译,则效率是十分低下的,那么是否可以提高TypeScript开发的效率,让不包含错误语法的TypeScript文件自动编译呢?这个问题可以通过开发工具VSCode解决。

对hello.ts文件代码做简单修改,具体如下。

ts 复制代码
function sayHello(person: string) {
    return 'Hello,' + person;
}

let user = [0, 1, 2, 3];
console.log(sayHello(user));

在终端中打开当前目录,并执行下方命令。

bash 复制代码
tsc --init

下面开始设置自动编译,共分为3步。

  1. 执行tsc-init命令后会生成一个tsconfig.json文件。该文件下的配置属性有很多个,找到noEmitOnError属性,该属性默认是注释状态,属性值为false,将其修改为true并解除注释状态就可以实现自动编译的功能。
  2. 点击VSCode菜单栏中的"终端"菜单,选择"运行任务"子菜单,此时页面中会出现一个下拉菜单,选择需要执行的文件。需要注意的是,如果之前已经运行过任务,下拉菜单中就会有最近使用过的任务记录;如果没有,则可以点击"显示所有任务"按钮。
  3. 在"所有任务"中找到tsc:监听hello的tsconfig.json配置选择项,点击即可运行。

此时还不能正常编译。这是因为之前代码中存在TypeScript语法检查出错的情况,现在编译仍旧会出错,在目录中不会生成hello.js文件。想要正常实现需求,就需要修复hello.ts文件中user的类型,将类型修改为string类型,此时编译将会自动执行,并成功生成hello.js文件。

4、TypeScript的类型

TypeScript内置了丰富的类型,包括string(字符串)​、number(数值)​、boolean(布尔值)​、array(数组)​、object(对象)​、function(函数)​、tuple(元组)​、enum(枚举)​、interface(接口)等。在定义变量时,我们可以对变量进行相应的类型约束。一旦添加类型约束,通过变量名加点的方式使用其属性或方法,VSCode编辑器就会有精准的补全提示;当变量的值不是声明的类型时,VSCode编辑器会有错误提示;当通过变量来访问其不存在的属性或方法时,VSCode编辑器也会有错误提示。

下面首先利用let声明一个变量hi,并给hi限定不同的类型,然后分别给hi赋值。值得一提的是,当操作hi时,VSCode编辑器会提示其对应的变量所拥有的属性和方法,比如当hi为string类型时,VSCode编辑器就会提示length属性,以及indexOf、match等方法;而当hi为number类型时,并且赋值为2015,hi就不会拥有length属性,而VSCode编辑器只会提示toFixed等数值方法;当hi为元素是string的array类型时,那么hi所拥有的属性或方法则包括length、join、keys等。下面分别进行演示。

将hi限定为string类型,并调用indexOf方法。

ts 复制代码
let hi:string = 'hello';

在操作变量hi时,VSCode编辑器的提示如图所示。

假如我们给一个string类型的变量赋了一个number类型的值,就违背了操作的初心。此时TypeScript的VSCode编辑器就会提示错误,并且明确给出错误的提示信息,以协助开发人员快速找到出现问题的地方。

因此在TypeScript中,使用特定类型显式地声明程序中定义的变量具有的属性和方法,并且每个变量都有一个类型。

JavaScript中的类型主要有两类,分别为基础类型和引用类型,而TypeScript是JavaScript的超集,因此它囊括了JavaScript中所有的基础类型和引用类型。不过在这个基础上,TypeScript还扩展了一些其特有的类型,如tuple(元组)​、enum(枚举)​、interface(接口)​、generic(泛型)等,这都是JavaScript不具有的。

5、TypeScript中的基础类型

JavaScript的基础类型主要包括string、number、boolean、null、undefined、symbol、bigint等。作为JavaScript的超集,TypeScript同样支持JavaScript中的所有基础类型,并扩展了any、unknown、void等其他基础类型。

5.1、TypeScript中与JavaScript一致的基础类型

TypeScript类型声明的具体操作,只需利用"​:类型"的方式就可以显式地声明变量的类型,那么现在只需明确TypeScript支持的类型即可。TypeScript中与JavaScript一致的基础类型主要包括string、number、boolean、undefined、null、symbol、bigint等。

其中string类型支持普通字符串和模板字符串的赋值;number类型需要注意各种不同的数值进制,其中二进制、八进制、十进制、十六进制等都是符合number类型约束规范的;boolean类型的赋值只支持true或者false;undefined类型和null类型都只有一个值,分别是undefined和null。这两种类型一般不会单独使用,而是与其他类型组合成联合类型来使用。

值得一提的是,symbol和bigint是ES6+提供的类型,而TypeScript也支持这两种类型。

请读者思考下面的代码。

ts 复制代码
//string
let myName: string = 'jerry';//普通字符串
let sayHi: string = `你好,我的名字时:${myName}`//模板字符串

//number
let binary: number = 0b1010//二进制
let octal: number = 0o744//八进制
let decimal: number = 10 //十进制
let hex: number = 0xf00d //十六进制
let infinity: number = Infinity //无穷

//boolean
let done: boolean = false //true、false是布尔值

//undefined
//给变量u这一undefined类型赋值undefined一般没有实际意义
let u: undefined = undefined

//null
let n: null = null //给变量n这一null类型赋值null一般没有实际意义

上面这些代码均可以正常运行。

请读者思考下面的代码。

ts 复制代码
//string
let myName: string = 'jerry';//普通字符串
let sayHi: string = `你好,我的名字时:${myName}`//模板字符串
//myName = 2015  //error

//number
let binary: number = 0b1010//二进制
let octal: number = 0o744//八进制
let decimal: number = 10 //十进制
let hex: number = 0xf00d //十六进制
let infinity: number = Infinity //无穷
//decimal = 'ts'  //error

//boolean
let done: boolean = false //true、false是布尔值
done = true
//done = 1 //error

//undefined
//给变量u这一undefined类型赋值undefined一般没有实际意义
let u: undefined = undefined

//null
let n: null = null 

//symbol
let s: symbol = Symbol('abc')

//bigint
let b: bigint = 2n

上面的代码违反了类型约束规范,进行了强制赋值处理。比如给string类型的变量赋值为2015,给number类型的变量赋值为"ts"​,给boolean类型的变量赋值为1等。这些错误在VSCode编辑器中会出现红色波浪线提示。

不过需要注意undefined和null,因为它们两个是任意类型的子类型,所以在一般情况下可以赋值为任意类型的值,并不会报错误提示。这个特点是以非strict模式为前提的,如果在tsconfig.json的"compilerOptions"编译选项里设置了""strict":true"属性,则不能将undefined赋值给非undefined类型的变量,以及不能将null赋值给非null类型的变量。

5.2、TypeScript中特有的基础类型

1. any

在某种场景下,我们需要为在编程阶段还不能明确类型的变量指定一个类型,此时就可以使用any类型。另外,某些值可能来自动态的内容,比如来自用户输入、第三方代码库等。在这种情况下,我们不希望类型检查器检查这些值,而是需要直接让它们通过编译阶段的检查,此时可以使用any类型来标记这些变量。演示代码如下。

ts 复制代码
let notSure: any = 4;
notSure = 'maybe a string';
notSure = false;

在改写代码时,any类型是十分有用的,它允许开发者在编译时有选择地包含或移除any类型检查,并且当开发者只知道一部分数据的类型时,any类型也是有用的,比如现在有一个数组,它包含了不同类型的数据,我们就可以使用any类型,具体如下。

ts 复制代码
let list: any[] = [1, 'abc', true, ()=>{}]

不过any类型声明的变量在使用时是没有一个确定的类型的,从而导致any类型没有任何的代码提示功能。

2. unknown

任何变量都可以设置为any类型,而any类型将会一路"绿灯"进行类型检查的放行处理,它不再检查类型,也没有语法提示。TypeScript还提供了一个特别的类型,即unknown,该类型与any类型的功能相反,它属于一路"红灯"限制。也就是说,在没有明确变量的类型之前,后面代码在使用时如果直接当作变量值的类型使用,则VSCode编辑器会报错误提示,演示代码如下。

ts 复制代码
let myName: unknown = 2015.8
let num = Math.round(myName) //error

我们可以先利用typeof进行类型判断,再使用,此时就可以正常通过,不会报错。

ts 复制代码
let myName: unknown = 2015.8
if (typeof myName === 'number') {
    let num = Math.round(myName)
}

**3. void **

void类型表示没有任何类型,该类型经常用来约束一个函数的返回值。值得一提的是,约束函数没有返回值,或者返回值是undefined。

ts 复制代码
function fn(): void {
    //不返回任何值或返回undefined都是合法的
    return undefined;
    //返回null或者其他任意值都是不合法的
    //return null
    // return 2
}

6、类型推断

如果我们没有为变量赋初始值,那么TypeScript环境会自动将变量的类型推断为any类型;如果我们定义了变量并赋予其初始值,但并没有为变量声明类型,那么TypeScript环境会自动将变量的类型推断为初始值的类型,这个过程被称为"类型推断"​。

我们尝试定义一个myName变量,不为其强制声明string类型,但为其设置string类型的初始值"atguigu"​,此时TypeScript会根据初始值"atguigu"将myName变量的类型推断为string。后续如果为myName变量赋予number类型的值,TypeScript就会显示错误,提示不能将number类型分配给string类型,这就是类型推断的强大魅力,具体代码如下。

ts 复制代码
let myName = 'atguigu';
myName = 2015 //error,不能将数值赋给string类型的变量

上面的代码等价于下面的代码。

ts 复制代码
let myName: string = 'atguigu';
myName = 2015 //error,不能将数值赋给string类型的变量

如果在声明变量时没有进行初始值赋值处理,那么无论之后有没有为变量赋值,变量都会被推断成any类型,后续操作都会将其类型当作any类型处理,不再做任何的类型检查,也没有任何类型的语法提示操作,代码如下。

ts 复制代码
let myName; //相当于let myName:any
myName = 'atguigu';
myName = 2015;

7、联合类型

联合类型(Union Types)表示变量的取值可以为多种类型中的任意一种,在类型定义时使用"|"分隔。比如在将myName变量声明为string类型或number类型中的任意一种类型时,就可以书写为"let myName:string|number"​,那么myName变量的类型就是string或number两者之一,但不能是其他类型,代码如下。

ts 复制代码
let myName: string | number;
myName = 'Tom';
myName = 2015;

myName = true;//error,不能将true赋值给string与number联合类型的变量

联合类型涉及多种类型,而不同类型的变量是有其不同属性的,string类型的变量有length属性、number类型的变量没有length属性。如果将myName变量声明为string或number类型的联合类型,那么其是否会有length属性呢?

尝试定义一个函数getLength,设置形参为myName,将其类型限定为"string|number"​,那么是否可以直接使用return返回myName.length属性呢?答案是:不可以。虽然string类型的变量有length属性,但number类型的变量是没有length属性的,因此会报错,代码如下。

ts 复制代码
function getLength(myName: string | number): number {
    return myName.length;//error,number类型的变量没有length属性
}

不过string和number类型的变量拥有一个共同的方法toString,调用共同方法不会有任何问题,代码如下。

ts 复制代码
function getString(myName: string | number): string {
    return myName.toString();
}

联合类型也可以结合TypeScript的类型推断实现相应功能,比如声明"let myName:string|number"​,没有为变量赋予初始值,类型推断会将其推断为any类型,但联合类型的声明并不会直接将其推断为any类型,而是会在赋值后根据联合类型中声明的类型进行推断。比如将myName变量赋值为"atguigu"string类型时,myName变量的类型就会被推断为联合类型中的string类型;如果将myName变量的类型赋值为"2015"number类型时,myName变量的类型又会被推断为联合类型中的number类型,因此VSCode编辑器将产生类型"number"的变量不存在属性"length"的错误提示,代码如下。

ts 复制代码
let myName: string | number;
myName = 'Tom';
console.log(myName.length); //3
myName = 2015;
console.log(myName.length); //error

8、类型断言

TypeScript通过类型断言这种方式告诉编译器"相信我,我知道自己在干什么"​。类型断言好比其他程序语言里的类型转换,但与类型转换不同的是,类型断言不进行特殊的数据检查。它在运行时不对代码产生影响,只在编译阶段起作用,因为TypeScript会假设程序员对代码已经进行了类型检查。类型断言有两种语法形式,分别是"尖括号"语法和as语法。

假如我们想要获取一个联合类型"number|string"变量的长度。虽然string类型的变量有length属性,number类型的变量没有length属性,但我们可以直接通过条件判断确认x.length是否可取,代码如下。

ts 复制代码
//需求:定义个函数得到一个string或number联合类型的变量的长度
//错误实现
function getLength(x: number | string) {
    //return x.length //error,number类型的变量没有length属性
    if(x.length) { //error,number类型的变量没有length属性
        return x.length;
    } else {
        return x.toString().length;
    }
}

为了让TypeScript认识到"相信我,我知道自己在干什么"​,可以尝试对变量进行类型断言。比如"x"的断言声明已经明确当变量x是string类型时,就进行条件判断,并且可以利用"(x as string)"将变量x进行string类型的强制转换,最终得到length属性。如果变量x是number类型,则可以直接利用number类型的变量拥有的toString方法先将其转化成字符串,再得到其length属性,代码如下。

ts 复制代码
//正确实现:类型断言
function getLength(x: number | string) {
    if((<string>x).length) {
        return (x as string).length;
    } else {
        return x.toString().length;
    }
}
console.log(getLength('Tom'));//3
console.log(getLength(2015));//4

9、数组和元组

9.1、数组

TypeScript可以像JavaScript一样操作数组元素,它定义数组的方式有两种:第1种是在元素类型后面写"​[​]​"​,表示由此类型元素组成的一个数组,代码如下。

ts 复制代码
let list1: number[] = [1, 2, 3];

第2种方式是使用数组泛型"Array<元素类型>"​,代码如下。

ts 复制代码
let list2: Array<number> = [1, 2, 3];

上面的两种方式都在定义数组时明确其数组元素的类型为number,所以数组中的元素只能是number类型,而不能是其他类型的。如果尝试给数组添加其他类型的数据,程序就会报错。

向数组中添加元素的代码如下。

ts 复制代码
list2.push(12);//正确
list2.push('abc');//错误

9.2、元组

元组可以表示一个已知元素数量和类型的数组,各元素的类型不必相同。比如,开发者可以定义一个值分别为string和number类型的元组。对于元组类型的变量,只有每个元素的类型一一匹配才能够顺利通过编译,元素个数与元素类型在不匹配的情况下,就会出现错误提示信息,代码如下。

ts 复制代码
let t1: [string, number];
t1 = ['hello', 10];//每个元素的类型匹配
t1 = ['hello', 10, 2];//元素个数多余定义的元素个数,报错
t1 = [10, 'hello']; //元素类型不匹配,报错

值得一提的是,当访问一个已知索引的元素时,除了可以得到正确的类型,还可以获取该类型下所有属性与方法的提示。

10、枚举

10.1、数值枚举

下面介绍数值枚举,一个枚举可以用一个关键字enum来定义。请读者思考下面的代码。

ts 复制代码
//默认是数值枚举
enum Color {
    Red,
    Yellow = 2,
    Green
}
console.log(Color);

let cat = {
    name: 'tom',
    color: 1
};

switch (cat.color) {
    case Color.Red:
        console.log('猫时红色的');
        break;
    case Color.Yellow:
        console.log('猫时黄色的');
        break;
    case Color.Green:
        console.log('猫时绿色的');
        break;
    default:
        console.log('猫时其他颜色的');
}
ts 复制代码
{
  '0': 'Red',
  '2': 'Yellow',
  '3': 'Green',
  Red: 0,
  Yellow: 2,
  Green: 3
}
猫时其他颜色的

在默认情况下,数值枚举从0开始为元素编号。当然,开发者也可以手动指定成员的数值。例如,我们将上面的例子改成从1开始为元素编号,代码如下。

ts 复制代码
enum Color {
	Red = 1,
	Green,
	Blue
}

此时Color中成员的值依次为1、2、3。还可以全部采用手动赋值,代码如下。

ts 复制代码
enum Color {
	Red = 1,
	Green = 2,
	Blue = 4
}

此时Color中成员的值依次为1、2、4。

枚举类型提供的一个功能是开发者可以通过枚举的值得到它的名字。例如,我们知道元素的数值为2,但是不确定它映射到Color里的哪个名字,就可以通过枚举查找相应的名字,代码如下。

ts 复制代码
enum Color {
    Red = 1,
    Green, 
    Blue
};
let colorName: string = Color[2] as string;
console.log(colorName);//Green

10.2、字符串枚举

字符串枚举要求为每个属性都指定一个特定的字符串值,类似于定义了几个常量,代码如下。

ts 复制代码
enum Status {
    padding = 'Padding',
    resolved = 'Resolved',
    rejected = 'Rejected'
};

let status: Status = Status.padding;
// status = 'abc'//error,不能是枚举之外的值

值得一提的是,上面的代码还为status赋予了一个枚举之外的值"abc"​,此时就会出现错误提示。而这种情况只限制在字符串枚举中,在数值枚举中则可以实现,这其实也是一个漏洞,只能期待后续TypeScript对其进行完善了。

11、函数

无论是在JavaScript中,还是在TypeScript中,函数都是非常重要的类型,只不过TypeScript为函数添加了额外的功能,让开发者可以更容易地使用。和JavaScript函数一样,TypeScript函数可以创建具名函数和匿名函数。此外,TypeScript函数还增加了函数类型、可选参数、默认参数及剩余参数的相关内容,下面分3个部分进行介绍。

11.1、函数类型

在JavaScript中我们会通过下面的代码来定义函数,具体如下。

js 复制代码
//具名函数
function sum(x, y) {
	return x + y
}

//匿名函数
let mySum = function(x, y) {
	return x + y
}

下面使用TypeScript为上面定义的函数的参数添加类型,代码如下。

ts 复制代码
function sum(x: number, y: number): number {
    return x + y;
}

let mySum = function(x: number, y: number): number {
    return x + y
}

上面的代码先为函数的每个参数添加类型,再为函数本身添加返回值类型。而TypeScript能够根据返回语句自动推断出返回值类型。

改写后mySum函数的完整类型如下。

ts 复制代码
let mySum2: (x: number, y: number) => number = function(
    x: number,
    y: number
): number {
    return x + y 
}

11.2、可选参数和默认参数

TypeScript中的每个函数参数默认都是必需的。编译器会检查开发者是否为每个参数都传入了值。简单地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。

在JavaScript中,每个参数都是可选的,在没有传参时,参数的值就是undefined。而在TypeScript中,我们可以在参数名后面添加"​?​"​,以实现可选参数的功能。比如,我们想让lastName是可选参数,就可以书写为"lastName?​:string"​。

在TypeScript中,开发者也可以为参数设置一个默认值。当开发者没有传递这个参数或传递的值是undefined时,就使用这个默认值,我们也将这样的参数叫作有默认初始值的参数。比如将firstName参数的默认值设置为"A"​,就可以书写为"firstName:string='A'"​。

ts 复制代码
function concatName(firstName: string='A', lastName?: string): string {
    if (lastName) {
        return firstName + '-' + lastName;
    } else {
        return firstName;
    }
}

console.log(concatName('c', 'd'));//c-d
console.log(concatName('c'));//c
console.log(concatName());//A

11.3、剩余参数

默认参数和可选参数有一个共同点:它们表示某一个参数。在某种场景下,如果想同时操作多个参数,或者并不能明确知道会有多少个参数传递进来,就可以使用剩余参数。

在TypeScript中,可以把所有参数收集到一个变量中,这样剩余参数会被当作个数不限的可选参数,代码如下。

ts 复制代码
function info(x: string, ...args: string[]) {
    console.log(x, args);
}
info('abc', 'c', 'd', 'e');//abc [ 'c', 'd', 'e' ]

12、接口

TypeScript的核心原则之一是对值所具有的结构进行类型检查,而接口(Interfaces)则可以实现这个功能,它可以用于定义对象的类型。接口是对象的状态(属性)和行为(方法)的抽象(描述)​。

12.1、接口初探

现有需求:创建"人"对象,需要对"人"的属性进行一定的约束,具体如下。

  • id是number类型,必须有,只读。
  • name是string类型,必须有。
  • age是number类型,必须有。
  • sex是string类型,可以没有。
  • sum是计算并返回两个数的和的方法,必须有。

接下来通过一个简单示例来观察接口是如何工作的,具体如下。

ts 复制代码
/*
    在TypeScript中,使用接口(Intefaces)来定义对象的类型
    接口时对象的状态(属性)和行为(方法)的抽象(描述)
    接口类型的对象:
        多了或者少了属性都是不允许的
        可选属性:?
        只读属性:readonly
*/

//定义 人 的接口
interface IPerson {
    id: number;
    name: string;
    age: number;
    sex: string;
    sum(x: number, y: number): number;
}

const person1: IPerson = {
    id: 1,
    name: 'tom',
    age: 20,
    sex: '男',
    sum(a: number, b: number): number {
        return a + b
    }
}

代码在运行时,类型检查器会查看对象内部的属性是否与IPerson接口描述一致,如果不一致,就会提示类型错误。

12.2、可选属性

接口里的属性不是必需的。有些属性在某些条件下存在,而在某些条件下根本不存在。假设只有sex属性不是必需的,那么代码可以书写为下方模式。

ts 复制代码
interface IPerson {
    id: number;
    name: string;
    age: number;
    sex?: string;
    sum(x: number, y: number): number;
}

其实带有可选属性的接口与普通接口的定义差不多,只是在可选属性名字的后面加了一个"​?​"​。

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获在引用了不存在的属性时出现的错误。比如下方代码没有在接口中定义age属性,VSCode编辑器则会报错,如图所示,而age属性不是必需的,可以没有。

ts 复制代码
interface IPerson {
    id: number;
    name: string;
    sex?: string;
    sum(x: number, y: number): number;
}

const person2: IPerson = {
    id: 1,
    name: 'tom',
    age: 20, //接口中未定义,报错
    // sex: '男', //可以没有
    sum(a: number, b: number): number {
        return a + b
    }
}

12.3、只读属性

一些对象属性的值只能在对象刚刚创建时被修改,针对该需求,可以在属性名字前面加上readonly指定其为只读属性,具体如下。

ts 复制代码
interface IPerson {
    readonly id: number
}

一旦属性被赋值后,就再也不能被改变了。

ts 复制代码
interface IPerson {
    readonly id: number
}

const person2: IPerson = {
    id: 2
}
person2.id = 2;//报错

12.4、描述函数类型

接口能够描述JavaScript中对象拥有的各种各样的外形。除了描述带有属性的普通对象,接口还可以描述函数类型。

为了使用接口描述函数类型,我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表中的每个参数都需要有名字和类型,代码如下。

ts 复制代码
//接口可以描述函数类型(参数的类型与返回值的类型)
interface SearchFn {
    (x: string, y: string): boolean;
}

这样定义后,我们可以像使用其他接口一样使用这个描述函数类型的接口。下方代码展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

ts 复制代码
//接口可以描述函数类型(参数的类型与返回值的类型)
interface SearchFn {
    (x: string, y: string): boolean;
}

const mySearch: SearchFn = function(a: string, b: string): boolean {
    return a.includes(b);
}

console.log(mySearch('abcd', 'bc'));//true

12.5、接口继承接口

接口可以相互继承。这让开发者能够从一个接口复制成员到另一个接口中,并且可以更灵活地将接口分割到可重用的模块中,代码如下。

ts 复制代码
interface Alarm {
    alert(): any;
}

interface Light {
    lightOn(): void;
    lightOff(): void;
}

//此时LightableAlarm接口就有了父接口的三个方法声明
interface LightableAlarm extends Alarm, Light {
    
}

13、类

对于传统的JavaScript程序,开发者可以使用函数和基于原型的继承来创建可重用的组件,但因为熟悉使用面向对象方式的开发者更习惯使用类的相关语法,所以使用这些语法就不会很顺畅。因此为了提升JavaScript的包容性,从ES6开始,允许JavaScript开发者使用基于类的面向对象的方式进行开发。在TypeScript中,允许开发者使用面向对象的相关特性,并且被编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下一个JavaScript版本。

下面来看一个使用类的代码,读者可先自行进行分析。

ts 复制代码
//类的基本定义与使用
class Person {
    //声明属性
    name: string;
    age: number;
    //构造函数
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    //一般方法
    sayInfo(): void {
        console.log(`我叫${this.name},今年${this.age}`);
    }
}
//创建类的实例对象,内部会调用Person的构造函数
const p = new Person('tom', 12);
//调用实例的方法
p.sayInfo();//我叫tom,今年12

如果读者学习过C#或Java,则会对上面这种语法非常熟悉。在上面的代码中声明了一个Person类,该类中有4个成员,分别是属性name、属性age、构造函数和sayInfo方法。

读者可能会注意到,在方法中引用任何一个类中定义的实例成员时都使用了this.成员名。这就表示访问的是实例成员。在代码中,我们使用new构造了Person类的一个实例对象。它会调用之前定义的构造函数,创建一个Person类型的新实例对象,并执行构造函数来初始化实例对象的属性。最后通过p对象调用其sayInfo方法。

13.1、继承

在TypeScript中,可以使用面向对象的方式编写代码。在基于类的程序设计中有一种最基本的模式是允许使用继承来扩展现有的类。请读者思考下面代码的运行结果。

ts 复制代码
//类的继承
class Animal {
    run (distance: number) {
        console.log(`Animal run ${distance}m`);
    }
}

class Dog extends Animal {
    cry () {
        console.log('wang!wang!');
    }
}

const dog = new Dog();
dog.cry();
dog.run(100);//可以调用父类中继承到的方法
// wang!wang!
// Animal run 100m

上面的代码展示了最基本的继承:类Dog从基类Animal中继承了属性和方法。其中Dog是一个派生类,它通过extends关键字派生自Animal基类。需要注意的是,派生类通常被称为子类,基类通常被称为父类。

在上面的代码中,因为Dog继承了Animal的功能,因此可以创建一个Dog的实例,该实例能够调用cry和run方法。

13.2、多态

所谓多态,就是由继承产生了相关的不同的类,对同一个方法可以有不同的响应。比如Snake类和Horse类都继承自Animal类,但是分别实现了自己的eat方法。此时针对某一个实例,开发者无须了解它是Cat还是Dog,就可以直接调用run方法,程序会自动判断应该如何执行run方法。对于多态简单的理解就是:声明某种类型的对象,但实际指定的可以是"某种类型"的对象,也可以是任意子类型的对象,也就是传入的对象有多种形态。

ts 复制代码
//父类型
class Animal {
    name: string 
    constructor (name: string) {
        this.name = name
    }
    run (distance: number = 0) {
        console.log(`${this.name} run ${distance}m`)
    }
}

//子类型
class Snake extends Animal {
    constructor(name: string) {
        //调用父类的构造函数
        super(name)
    }
    //重写父类型的方法
    run (distance: number = 5) {
        console.log('sliding...')
        super.run(distance)
    }
}

//子类型
class Horse extends Animal {
    constructor(name: string) {
        super(name)
    }
    run(distance: number = 50) {
        console.log('dashing...')
        super.run(distance)
    }
}

//声明接收父类型对象
//多态:多种形态=>声明需要的时一个Animal类型的对象
//实际可传入animal/horse/snake,最终调用的都是实际对象的方法
function run (animal: Animal) {
    animal.run()
}

run(new Animal('aa'));
run(new Snake('ss'));
run(new Horse('hh'));
// aa run 0m
// sliding...
// ss run 5m
// dashing...
// hh run 50m

13.3、访问修饰符

  1. public
    当我们在类中定义成员时,如果不使用任何访问修饰符,则默认是public,当然也可以显式地写上public。一般称该成员为公开的属性或方法,访问它是不受限制的,在类体内部或类体外部都可以通过实例对象来访问它。
  2. private
    当成员被标记成private时,一般被称为私有的属性或方法,只能在类体内部被访问,出了类体就不能被访问了。
  3. protected
    protected修饰符与private修饰符的作用相似,但有一点不同,protected成员在派生的子类中仍旧可以被访问。

下面通过一段代码演示上面3种访问修饰符的用法。

ts 复制代码
/*
    访问修饰符:用来描述类内部的属性或方法的可访问性
    public:默认是,公开的,类体内部或外部都可以访问
    private:只有类体内部才可以访问
    protected:类体内部或子类可以访问
*/

class Animal {
    public name: string;
    public constructor (name: string) {
        this.name = name;
    }
    public run (distance: number = 0) {
        console.log(`${this.name} run ${distance}m`);
    }
}

class Person extends Animal {
    private age: number = 18;
    protected sex: string = '男';

    run (distance: number = 5) {
        console.log('Person jumping...');
        super.run(distance);
    }
}

class Student extends Person {
    run (distance: number = 6) {
        console.log('Student jumping...');
        console.log(this.sex);//子类能看到父类中受保护的成员
        // console.log(this.age);//子类看不到父类中私有的成员
        super.run(distance);
    }
}

console.log(new Person('abc').name);//公开的,可见
// console.log(new Person('abc').sex);//受保护的,不可见
// console.log(new Person('abc').age);//私有的,不可见

13.4、readonly修饰符

readonly修饰符可以将属性设置为只读的。需要注意的是,只读属性必须在声明时或在构造函数中被初始化。

ts 复制代码
class Person {
    readonly name: string = 'abc';
    constructor (name: string) {
        this.name = name;
    }
}

let john = new Person('John');
// john.name = 'peter';//error

13.5、静态属性

其实,TypeScript还可以创建类的静态属性,这些属性存在于类本身而不是类的实例对象上,只需利用static关键字定义属性即可。比如通过static关键字定义Person类中的name2属性,当要访问这个属性时,需要通过Person.name2来访问,而没有添加static关键字的属性是非静态属性,需要通过类的实例对象来访问。

ts 复制代码
//静态属性是类本身的属性
//非静态属性是类的实例对象的属性

class Person {
    name1: string = 'A';
    static name2: string = 'B';
}

console.log(Person.name2);//B
console.log(new Person().name1);//A

13.6、抽象类

场景:有一种类型,它有确定的行为,也有不确定的行为,对于这种场景就可以使用抽象类来实现。

抽象类主要作为其他子类的父类使用,不能被实例化。不同于接口,抽象类可以包含成员的实现细节。可以通过关键字abstract定义抽象类和抽象类中的抽象方法。

ts 复制代码
abstract class Animal {
    abstract cry (): any;
    run () {
        console.log('run()');
    }
}

class Dog extends Animal {
    cry () {
        console.log(' Dog cry()')
    }
}

const dog = new Dog();
dog.cry();// Dog cry()
dog.run();//run()

14、泛型

泛型与接口类似,不同之处在于,泛型没有属性和方法声明。当定义函数、接口或类时,如果有不确定的类型,就可以使用泛型,如果不使用泛型,就只能将其指定为any类型。由于在声明时指定类型为any类型,在使用具体数据时没有任何提示(错误/正确)​,因此泛型通常被应用在定义函数、接口或类时,不预先指定具体类型,而是在使用时再指定具体类型的情景中。

14.1、泛型函数

现有需求:函数根据指定的数量count和数据value,创建一个包含count个value的数组。

在学习泛型之前,我们先看下面的代码。

ts 复制代码
function createArray(value: any, count: number): any[] {
    const arr: any[] = [];
    for (let index = 0; index < count; index++) {
        arr.push(value);
    }
    return arr;
}

const arr1 = createArray(11.12, 3);
const arr2 = createArray('abcd', 3);
console.log(arr1, arr2);
//[ 11.12, 11.12, 11.12 ] [ 'abcd', 'abcd', 'abcd' ]

上面代码的功能没有问题,但由于函数声明的返回值为any[​]​,因此从arr1或arr2中取出的元素是any类型的,当我们想对元素进行进一步操作时,就不会再有类型检查的提示了。

如何才能有类型检查的提示呢?此时就可以使用泛型。

在使用泛型实现上面的需求之前,先来明确"使用泛型三部曲"​,分别是定义泛型、使用泛型和指定具体类型,具体如下。

  1. 定义泛型:在定义的函数名右侧定义泛型,比如"fn<T>"。
  2. 使用泛型:形参、返回值、函数体等位置。
  3. 指定具体类型:调用函数时在函数名的右侧指定具体类型,比如"fn<number>()"。

此时再来实现上面的需求,代码如下。

ts 复制代码
function createArray2<T> (value: T, count: number): T[] {
    const arr: Array<T> = [];
    for (let index = 0; index < count; index++) {
        arr.push(value);
    }
    return arr;
}

const arr3 = createArray2(11.12, 3);
const arr4 = createArray2('abcd', 3);
console.log(arr3, arr4);
//正确语法有补全提示
console.log(arr3[0]?.toFixed(1), arr4[0]?.split(''));

在一个函数中可以定义多个泛型参数,示例代码如下。

ts 复制代码
function swap <K, V> (a: K, b: V): [K, V] {
	return [a, b];
}
const result = swap<string, number>('abc', 123);
console.log(result[0].length, result[1].toFixed());

14.2、泛型接口

接口中的属性或方法可能存在不确定的类型,这时就可以为接口定义泛型。只需在接口的属性或方法声明中引用这个泛型,在定义接口的实现类时指定泛型的具体类型即可。

比如,现在我们需要定义一个接口来包含不确定类型的对象数组,以及对象的增、删、改、查的方法,这里我们只演示添加和根据id查找两种情况。

由于接口内部要管理的数据对象的类型是不确定的,因此我们可以为这个接口定义泛型T,内部的属性data为要管理的数据对象的数组,指定它为T类型的数组。add方法中接收的参数为数据对象,它的类型为T类型。getById方法的返回值为查找到的一个数据对象,因此将返回值声明为T类型。

ts 复制代码
interface IbaseCRUD<T> {
    data: T[];
    add: (t: T) => void;
    getById: (id: number) => T | undefined;
}

后面添加了不同的类型,比如用户类型User。

ts 复制代码
class User {
    id?: number; 
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age; 
    }
}

我们在定义操作User对象的CRUD类时,需要实现前面定义的泛型接口,此时就需要指定泛型的具体类型为User。这样就会约束我们定义的User数组类型的data属性、add方法和getById方法,且add方法接收的参数必须是User类型的,getById方法的返回值必须是User类型的。

ts 复制代码
interface IbaseCRUD<T> {
    data: T[];
    add: (t: T) => void;
    getById: (id: number) => T | undefined;
}

class User {
    id?: number; 
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age; 
    }
}

//定义操作User对象的CRUD类UserCRUD
class UserCRUD implements IbaseCRUD<User> {
    data: User[] = [];

    add(user: User): void {
        user = {...user, id: Date.now()};
        this.data.push(user);
        console.log('保存user', user.id);
    }

    getById(id: number): User | undefined {
        return this.data.find((item) => item.id === id);
    }
}

//测试使用
const userCRUD = new UserCRUD();
userCRUD.add(new User('tom', 12));
userCRUD.add(new User('jerry', 22));
console.log(userCRUD.data);

// 保存user 1778058622676
// 保存user 1778058622686
// [
//   { id: 1778058622676, name: 'tom', age: 12 },
//   { id: 1778058622686, name: 'jerry', age: 22 }
// ]

这样利用泛型就实现了对指定类型进行约束的效果。如果我们需要扩展定义其他类型的数据和对应的CRUD类,只需要实现此泛型接口,就可以起到完美的约束效果。

14.3、泛型类

如果在定义类时,类中定义的属性、类中方法的参数或返回值是不确定的,就可以使用泛型类,并且具有泛型类型的属性可以没有属性值,具有泛型类型的方法可以没有方法体。

现在我们想要实现这样的类:包含一个初始值initValue属性,但该属性值的类型是不确定的,还包含一个add方法,其接收的两个参数和返回值是不确定的,但它们都与initValue属性值的类型是一致的。

此时就可以使用泛型类来定义。在类名右侧通过来定义泛型类,类的实例属性initValue、add方法的形参和返回值都引用了泛型类型T。

ts 复制代码
//定义类,指定泛型类型
class GenericData<T> {
    //在类体中使用泛型类型
    initValue: T;
    add: (x: T, y: T) => T;

    constructor(initValue: T, add: (x: T, y: T) => T) {
        this.initValue = initValue;
        this.add = add;
    }
}

接下来可以创建泛型类的实例,在类名右侧指定具体类型,随后指定实例的initValue属性和add方法。如果指定的具体类型是number,那么initValue属性值和add方法接收的参数和返回值都必须是number类型的。同理,如果指定的具体类型是string,那么initValue属性值和add方法接收的参数和返回值都必须是string类型的。

ts 复制代码
//定义类,指定泛型类型
class GenericData<T> {
    //在类体中使用泛型类型
    initValue: T;
    add: (x: T, y: T) => T;

    constructor(initValue: T, add: (x: T, y: T) => T) {
        this.initValue = initValue;
        this.add = add;
    }
}

//在创建实例时指定泛型具体类型
const gNumber = new GenericData<number>(4, (a, b) => a + b);
console.log(gNumber.add(gNumber.initValue, 5));//9

const gString = new GenericData<string>('abc', (a, b) => a + b);
console.log(gString.add(gString.initValue, 'cba'));//abccba

14.4、泛型约束

泛型是没有属性和方法声明的。如果直接读取一个泛型变量的某个属性,程序就会报错,因为这个泛型变量根本就不知道它有这个属性。比如下面的代码读取了一个泛型变量的length属性,程序就会报错。

ts 复制代码
function fn<T> (x: T): void {
	console.log(x.length);//error
}

上面代码出现的问题可以使用泛型约束来解决。所谓泛型约束,是指在定义泛型时,指定其继承特定接口。这样泛型就能从父接口中继承特定属性或方法了。

重写上面的代码,定义包含length属性的接口Lengthable,在函数fn2定义泛型T时,指定T继承Lengthable接口,这样具有T类型的x变量就有了length属性,类型检查就可以正常通过,代码如下。

ts 复制代码
interface Lengthable {
    length: number;
}

//指定泛型约束
function fn2 <T extends Lengthable> (x: T): void {
    console.log(x.length);
}

在调用函数fn2时,需要传入符合约束类型的值,并且该值必须包含length属性。

ts 复制代码
fn2('abc');
// fn2(123);//error,number类型不包含length属性

15、其他常用语法

15.1、类型别名

类型别名是指给特定的值或类型通过type来指定别名,在后续代码中可以通过别名来代表这个值或类型。类型别名并没有定义新的类型,只是为了方便使用友好名称。

下面的代码演示了值类型别名和类型别名两种情况。

ts 复制代码
//1. 值类型别名
type Status = 1 | 2 | 3;//使用Status来代表1/2/3的可选值别名

//不使用值类型别名
let status: 1 | 2 | 3 = 2;
//status = 4; //error

//使用值类型别名
let status2: Status = 3;
//status = 5; //error

//2. 类型别名
type Key = string | number;
let a: Key = 'abc';
a = 123;

interface Person {
    username: string;
    age: number;
}
type Persons = Person[]; //使用Persons代表Person[]

//不使用类型别名
let persons: Person[] = [
    {username: 'tom', age: 12},
    {username: 'jack', age: 13}
];
//使用类型别名
let persons2: Persons = [
    {username: 'tom', age: 12},
    {username: 'jack', age: 13}
]

15.2、获取类型

在JavaScript中可以通过typeof获取一个表达式的类型名称;在TypeScript中,可以通过typeof获取一个表达式的类型,并且typeof只能用于类型约束。

ts 复制代码
const person = {
    name: 'tom',
    age: 12
}

function fn(x: number): string {
    return x + 'abc';
}

//JavaScript的typeof
console.log(typeof person);//'object
console.log(typeof fn);//'function'
console.log(typeof person.name);//'string'

在TypeScript中,也可以为使用typeof获取的类型定义类型别名,并通过类型别名来进行类型约束,代码如下。

ts 复制代码
type Person = typeof person;
let d: Person = {
    name: 'bob',
    age: 15
}

15.3、内置对象类型

JavaScript中的内置对象类型在TypeScript中可以直接被当作定义好的类型使用。内置对象类型包括ECMAScript和Web API在全局作用域中添加的一些对象类型。下面将

1. ECMAScript的内置对象类型

ECMAScript的内置对象类型有Boolean、Number、String、Date、RegExp和Error等,直接来看案例。

ts 复制代码
//ECMAScript的内置对象类型
let b: Boolean = new Boolean(1);
let n: Number = new Number(true);
let s: String = new String('abc');
let d: Date = new Date();
let r: RegExp = /^1/;
let e: Error = new Error('error message');
b = true;
// let bb: boolean = new Boolean(2);//error

2. Web API的内置对象类型

Web API的内置对象类型有Document、HTMLElement、DocumentFragment、MouseEvent和NodeList等,直接来看案例。

ts 复制代码
/*
    document就是Document类型的对象
    div就是HTMLElement类型的对象
    divs就是NodeList类型的对象
    fragment就是DocumentFragment类型的对象
    event就是MouseEvent类型的对象
*/
const div: HTMLElement = document.getElementById('test');
const divs: NodeList = document.querySelectorAll('div');
const handleClick = (event: MouseEvent) => {
    console.log(event.target);
}
document.addEventListener('click', handleClick);
const fragment: DocumentFragment = document.createDocumentFragment();

15.4、声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。声明文件是一个包含声明语句的单独文件,声明语句主要用来对新的语法进行检查,简单来说,就是加载对应类型的说明代码。

比如我们想使用第三方库jQuery,就需要首先在HTML中通过script标签引入jQuery,然后就可以使用全局变量""或"jQuery"了。而在TypeScript中,编译器并不认识全局变量""或"jQuery"​,代码如下。

ts 复制代码
jQuery('#foo');
//Error:Cannot find name 'jQuery'.

此时就可以通过"declare var"来定义"jQuery"的类型,代码如下。

ts 复制代码
declare var jQuery: (selector: string) => any;
jQuery('#foo');

在通常情况下,声明文件都会单独写成一个xxx.d.ts文件。下面创建jQuery.d.ts文件,将声明语句定义在其中,TypeScript编译器会扫描并加载项目中所有的TypeScript声明文件。

ts 复制代码
declare var jQuery: (selector: string) => any;

其实很多第三方库都定义了对应的声明文件库,库文件名一般为@types/xxx,读者可以自行在npm中央仓库中搜索。值得注意的是,有的第三方库在下载时会自动下载对应的声明文件库,如Webpack,而有的第三方库则需要单独下载对应的声明文件库,如jQuery。可以通过下方命令下载jQuery库对应的声明文件库。

ts 复制代码
npm i -D @types/jquery

15.5、常用操作符

TypeScript提供了4个非常有用的操作符:

  • 可选链操作符
  • 空值合并操作符
  • 非空断言操作符
  • 非空断言链操作符。
  1. 可选链操作符对应的是"?.",它的使用语法为"表达式?.xxx"或"函数表达式?.(​)",它的作用为:如果左侧表达式的值为undefined或null,则结果为undefined,不会抛出错误,否则正常处理。
ts 复制代码
interface State {
    name?: string;//name的值为string或undefined类型
    fn?(): void; //fn的值为函数或undefined类型
}

//指定State类型的s对象,并指定name和fn
let s: State = {
    name: 'tom',
    fn (): void {
        console.log('fn')
    }
}

//错误写法:不使用?.
// s.name.length
// s.fn()

//正确写法:使用?.
console.log(s.name?.length);//3
console.log(s.fn?.());//undefined
  1. 空值合并操作符对应的是"?​?",它的使用语法为"表达式1?​?表达式2",它的作用为:如果左侧表达式1的值为null或undefined,则返回右侧表达式2的值,否则返回左侧表达式1的值。下面来进行代码测试。
ts 复制代码
const person = {
    name: null,
    age: undefined
}

//当左侧表达式1的值为null或undefined类型时,返回右侧表达式2的值
const a1 = person.name ?? 'A'; ///'A
const a2 = person.age ?? 18;//18
console.log(a1, a2);

//当左侧表达式1的值不为null和undefined类型时,返回左侧表达式1的值
const a3 = false ?? 'B';//false
const a4 = 0 ?? 1;//0
console.log(a3, a4);
  1. 非空断言操作符对应的是"",它的使用语法为"表达式!",它的作用为:通知TypeScript编译器,左侧表达式的值不会为null或undefined类型,从而避免编译器报错。下面来进行代码测试。
ts 复制代码
/*
    有问题:根据id获取元素的结果值类型为HTMLElement | null,也就是可能为null类型
    之后container1不能直接进行.操作
*/
const container1 = document.getElementById('container');
container1.textContent = 'Hello';//error

//加上!后,TypeScript编译器看到的就是HTMLElement类型,之后containe2可以进行.操作
const container2 = document.getElementById('container')!;
container2.textContent = 'Hello';
  1. 非空断言链操作符对应的是"!.",它的使用语法为"表达式!.xxx=value"或"表达式!.xxx(​)",它的作用为:断言表达式的值不会为null或undefined类型,整个表达式可以被赋值或作为对象调用内部方法。下面来进行代码测试。
ts 复制代码
const container3 = document.getElementById('container');
container3!.textContent = 'Hello!';
container3!.getAttribute('name');

需要注意的是,非空断言后即使这个container元素是不存在的,TypeScript的编译也是可以通过的,但在运行时会报错。

相关推荐
小李子呢02112 小时前
前端八股Vue---Vue-router路由管理器
前端·javascript·vue.js
百锦再3 小时前
Auto.js变成基础知识学习
开发语言·javascript·学习·sqlite·kotlin·android studio·数据库开发
kyriewen115 小时前
你等的Babel编译,够喝三杯咖啡了——用Rust重写的SWC,只需眨个眼
开发语言·前端·javascript·后端·性能优化·rust·前端框架
逍遥德6 小时前
AI时代,计算机专业大学生学习指南
java·javascript·人工智能·学习·ai编程
Rkgua6 小时前
JS中模拟函数重载的使用
javascript·jquery
竹林8186 小时前
用 wagmi v2 和 Next.js 14 硬扛 NFT 市场前端:从合约调用失败到批量上架,我踩了这些坑
javascript·next.js
「已注销」7 小时前
面试分享:二本靠7轮面试成功拿下大厂P6
前端·javascript·面试
walking9578 小时前
重新学习前端之设计模式与架构
前端·javascript·面试
walking9578 小时前
重新学习前端之TypeScript
前端·javascript·面试