TypeScript
TypeScript是微软发行的一款脚本语言,它是JavaScript的超集。TypeScript相比JavaScript,为 JS 添加了类型支持,可以在任何运行 JavaScript 的地方运行。
JS 的类型系统存在“先天缺陷”弱类型,JS 代码中绝大部分错误都是类型错误,而且开发的时候,定义的变量本应该就有类型。这些经常出现的错误,导致了在使用 JS 进行项目开发时,增加了找 Bug、改 Bug 的时间,严重影响开发效率。从编程语言的动静来区分,TypeScript 属于静态类型的编程语言 JavaScript 属于动态类型的编程语言。并且,配合 VSCode 等开发工具,TS 可以提前到在编写代码的同时就发现代码中的错误,减少找 Bug、改 Bug 时间。
配置TypeScript运行环境
安装TypeScript编译器
Node.js/浏览器,只认识 JS 代码,不认识 TS 代码。需要先将 TS 代码转化为 JS 代码,然后才能运行。
我们可以使用npm全局安装ts编译器。
npm install -g typescript
全局安装ts编译器后,就可以使用tsc编译ts代码,使用tsserver启动ts服务器。
tsc --version # 查看tsc版本
尝试编译TypeScript代码
我们创建一个index.ts文件,然后输入以下的代码。
let strings: Array<string> = ['a', 'b', 123];
console.log(strings);
随后用终端,运行如下命令,编译index.ts。
tsc index.ts
在编译时,它提示了一个错误。
index.ts:1:41 - error TS2322: Type 'number' is not assignable to type 'string'.
1 let strings: Array<string> = ['a', 'b', 123];
~~~
Found 1 error in index.ts:1
它的意思是123不是数组元素指定的类型string。由此我们可以看到,
我们将该代码改为正确的。
let strings: Array<string> = ['a', 'b', 'c'];
console.log(strings);
重新运行编译index.ts命令,可以看到在index.ts目录下多了一个index.js。
var strings = ['a', 'b', 'c'];
console.log(strings);
这就是编译TS的过程。
准备运行TS项目
在学习TS的过程中,频繁使用tsc命令显然不太方便,我们可以依赖于自动工具。我们可以创建一个基于vue的ts文件,然后启动vue服务器,TS输出的内容就可以在网页中实时预览了。
我们可以执行以下命令初始化Vue项目,然后修改main.ts,即可实时运行TS文件。
npm init vite@latest
√ Project name: ... ts-demo
√ Select a framework: » Vue
√ Select a variant: » TypeScript
Scaffolding project in A:\JetBrainsProjects\Test\ts-demo...
Done. Now run:
cd ts-demo
npm install
npm run dev
执行cd ts-demo和npm install后,前往项目根目录,将src内除main.ts外的所有文件都删除,然后删除main.ts内的代码,最后运行vite,启动开发服务器,我们就可以将main.ts内的代码输出到页面中了。
基础
类型注解
TypeScript比JavaScript多出的就是类型限制,它将JavaScript变成了一款强类型语言。
TypeScript为变量添加类型限制是通过类型注解实现的,类型注解的语法是let/const 变量名 : 类型
。
let age: number = 18
为变量添加类型后,为它赋值一个字符串类型的常量就会报错。
let age: number = "18"
TypeScript的数据类型
TS中的常用基础类型分为两类:
- JS 已有类型:原始类型,简单类型( number/string/boolean/null/undefined ),复杂数据类型(数组,对象,函数等)。
- TS 新增类型:联合类型,自定义类型(类型别名),接口,元组,字面量类型,枚举,void等。
原始数据类型的用法和JavaScript的用法一致。
let age: number = 18
let myName: string = '老师'
let isLoading: boolean = false
新增类型将在下面逐一介绍。
数组
TS数组的声明有两种语法。
// 写法一:
let numbers: number[] = [1, 3, 5]
// 写法二:
let strings: Array<string> = ['a', 'b', 'c']
联合类型
联合类型表示一个元素可以有多种类型,对于数组,则可以允许数组包含不同元素,如同时包含字符串和数字。
TS中,使用“|”表示联合类型。|(竖线)在 TS 中叫做联合类型,即:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种。
let timer: number | null = null
let arr: (number | string)[] = [1, 'a', 3, 'b']
类型别名
若类型的种类过于复杂,我们可以自定义一个类型。创建类型别名后,直接使用该类型别名作为变量的类型注解即可。
TS使用type关键字自定义类型,语法是type 自定义类型名 = 其他类型
。
type CustomArray = (number | string)[]
let arr : CustomArray = [1, "b", 3, "d"]
这里注意,自定义类型名也是变量名称,要遵守定义变量名称的规则,如不能是关键字。
函数
参数和返回值类型
函数的类型实际上指的是:函数参数和返回值的类型。为函数指定类型的两种方式:单独指定参数或返回值的类型;同时指定参数、返回值的类型。
- 单独指定参数或返回值的类型:
// 函数声明
function add(num1: number, num2: number): number {
return num1 + num2
}
// 箭头函数
const add = (num1: number, num2: number): number => {
return num1 + num2
}
- 同时指定参数、返回值的类型:
type AddFn = (num1: number, num2: number) => number
const add: AddFn = (num1, num2) => {
return num1 + num2
}
当函数作为表达式时,可以通过类似箭头函数形式的语法来为函数添加类型。
这种形式只适用于函数表达式。
封装函数参数类型
若多个函数的参数和返回值类型相同,则可以将它封装成一个自定义类型。自定义类型只能用于赋值表达式,不能用于函数声明。
type myFuncType = (a: number, b: number) => number
// 这种函数不能用自定义函数类型
function add1(a: number, b: number): number {
return a + b
}
const add2: myFuncType = function(c, d) {
return c + d
}
const add3: myFuncType = (c, d) => c + d
不同形式的函数
普通函数、匿名函数和箭头函数定义参数和返回值的位置如下例。
function add(param1: number, param2: number): number {
return param1 + param2
}
const myPrint = function (myArguments: string[]): void {
console.log(...myArguments)
}
const myFunc = (myName: string, myAge: number): void => {
console.log(`${myName}今天${myAge}岁了`)
}
在TS中,箭头函数的括号不能省略。
void类型
如果函数没有返回值,那么,函数返回值类型为:void。如果一个函数没有返回值,在TS的类型中,应该使用void类型,而不是undefined。
function greet(name: string): void {
console.log('Hello', name)
}
如果什么都不写,函数的返回值类型为:void。
const add = () => {}
这种写法是明确指定函数返回值类型为void,与上面不指定返回值类型相同。
const add = (): void => {}
如果指定返回值类型为undefined,函数体中必须return undefined。
const add = (): undefined => {
return undefined
}
可变参数
使用函数实现某个功能时,参数可以传也可以不传。这种情况下,在给函数参数指定类型时,就用到可选参数了。
比如数组的 slice 方法,可以 slice() 也可以 slice(1) 还可以 slice(1, 3)。它的定义如下。
function mySlice(start?: number, end?: number): void {
console.log('起始索引:', start, '结束索引:', end)
}
mySlice(1, ); // start为1,end为undefined。
定义可选参数的语法就是在参数后面加一个问号"?"。
注意,可选参数必须在必选参数的后面,如下面的语法是错误的。
function mySlice(start?: number, end: number): void {
console.log('起始索引:', start, '结束索引:', end)
}
对象类型
JS中的对象是由属性和方法构成的,而TS对象的类型就是在描述对象的结构(有什么类型的属性和方法)。
几种对象类型的写法如下。
// 空对象
let person: {} = {}
// 有属性的对象
let person: { name: string } = {
name: '同学'
}
// 既有属性又有方法的对象
// 在一行代码中指定对象的多个属性类型时,使用 `;`(分号)来分隔
let person: { name: string; sayHi(): void } = {
name: 'jack',
sayHi() {
}
}
// 对象中如果有多个类型,可以换行写:
// 通过换行来分隔多个属性类型,可以去掉 `;`
let person: {
name: string
sayHi(): void
} = {
name: 'jack',
sayHi() {
}
}
使用对象类型在于:
- 使用 {} 来描述对象结构
- 属性采用 属性名: 类型的形式
- 方法采用 方法名(): 返回值类型的形式
封装对象类型
我们也可以将对象类型封装成自定义类型。
type Person = {
name: string,
age: number
}
let person: Person = {
name: "张三",
age: 19
}
对象的属性有多种多样,包括普通成员变量、成员函数等,以及可选变量,它们的定义语法如下。
type MyObject = {
name: string,
age: number,
sayHi: () => void, //成员函数类型
gender?: string //可选属性
}
let myObj:MyObject = {
name: "张三",
age: 18,
sayHi: function () {
console.log(`${this.name}的年龄为${this.age}`)
},
gender: "男"
}
console.log(myObj)
myObj.sayHi()
使用对象的可选属性
在声明对象的类型时,使用了英文问号代表此属性可选。在大型项目中,我们可能无法确定一个对象是否有此属性,往往需要加一个if判断。在TS中我们可以用以下几种方法尝试使用对象可能存在的属性。
// 通过if判断
if (obj.val) {
console.log(obj.val)
}
// 通过逻辑与
obj.val && console.log(obj.val)
// 通过?符号,它只能在链式调用的中间使用
obj.someField?.func() //相当于obj.someField && obj.someField.func()
接口
接口实际上比type类型别名更早出现,接口是用来约束对象的属性的,不过现在使用type更多。
使用interface关键字定义接口,其他语法和type运算符基本一致。
interface Person {
name: string
age: number
gender?: string
sayHi: () => void
}
let person: Person = {
name: "张三",
age: 18,
sayHi: function () {
console.log(`你好,我是${this.name},我今年${this.age}岁`)
}
}
person.sayHi()
接口继承
若两个对象有相同的属性,而一个对象比另一个对象多一些属性,那么前者就可以继承自后者,将公共属性提取出来做成父接口,然后每一个子接口在继承自父接口的同时,可以拥有自己的属性。
TypeScript使用extends关键字继承接口。继承后,父接口中有的属性,子接口就不需要重复定义了。
interface Person {
name: string
age: number
}
interface Student extends Person {
class: string,
score: number
}
let student : Student = {
name: "张三",
age: 18,
class: "三年一班",
score: 100
}
元组类型
元组类型可以理解为不能改变的列表类型。元组的定义为[元素类型1, 元素类型2, ...]
,中括号不变,元素类型可以有多种。在实例化元组时,元素的类型和顺序必须和元组定义的一致。
type Position = [number, number]
let point = [1, 2]
类型推断
若在声明变量时没有明确指定变量类型时,若为元素赋值,则TS会自动推断元素为什么类型,然后隐式地限制它的类型。
// 变量 age 的类型被自动推断为:number
let age = 18
// 函数返回值的类型被自动推断为:number
function add(num1: number, num2: number): number {
return num1 + num2
}
在日常开发中,我们可以选择省略类型注解,充分利用TS类型推论的能力,提升开发效率。如果不知道类型,可以通过鼠标放在变量名称上,利用 VSCode 的提示来查看类型。
字面量类型
下面的代码,两个变量的类型不一致。
let str1 = 'Hello TS'
const str2 = 'Hello TS
通过 TS 类型推论机制,可以得到答案:
- 变量 str1 的类型为:string
- 变量 str2 的类型为:'Hello TS'
原因:
- str1 是一个变量(let),它的值可以是任意字符串,所以类型为:string。
- str2 是一个常量(const),它的值不能变化只能是 'Hello TS',所以,它的类型为:'Hello TS'。
注意:此处的 'Hello TS',就是一个字面量类型,也就是说某个特定的字符串也可以作为 TS 中的类型。
任意的 JS 字面量(比如,对象、数字等)都可以作为类型使用。
字面量: { name: 'jack' } [] 18 20 'abc' false function() {}
枚举类型
枚举的功能类似于字面量类型+联合类型组合的功能,也可以表示一组明确的可选值。
TypeScript使用enum关键字定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个。
// 创建枚举
enum Direction { Up, Down, Left, Right }
// 使用枚举类型
function changeDirection(direction: Direction) {
console.log(direction)
}
// 调用函数时,需要应该传入:枚举 Direction 成员的任意一个
// 类似于 JS 中的对象,直接通过 点(.)语法 访问枚举的成员
changeDirection(Direction.Up)
- 使用 enum 关键字定义枚举
- 约定枚举名称以大写字母开头
- 枚举中的多个值之间通过 , (逗号)分隔
- 定义好枚举后,直接使用枚举名称作为类型注解
枚举的实现
枚举内部是通过数字实现的。从第一个元素开始,每个元素的值依次是0、1、2,依次类推。我们也可以将其显式地赋一个值,以满足我们的更多需求。
enum Direction {
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right"
}
console.log(Direction.Up)
上述TS代码编译后的JS代码如下。
var Direction;
(function (Direction) {
Direction["Up"] = "Up";
Direction["Down"] = "Down";
Direction["Left"] = "Left";
Direction["Right"] = "Right";
})(Direction || (Direction = {}));
console.log(Direction.Up);
any类型
any类型代表这个变量可以为任何类型。我们不推荐使用any类型,因为这样会失去TS为变量的类型做保护的机制。
若一个元素为any类型,我们可以对该值进行任何操作,并且没有错误提示。
let obj: any = {x: 0}
obj.bar = 100
obj() // 编译不会有错误 运行才有错误
尽可能的避免使用 any 类型,除非临时使用 any 来“避免”书写很长、很复杂的类型。
其他隐式具有 any 类型的情况:
- 声明变量不提供类型也不提供默认值
- 函数参数不加类型
注意:因为不推荐使用 any,所以,这两种情况下都应该提供类型。
类型断言
有时候我们会比 TS 更加明确一个值的类型,此时,可以使用类型断言来指定更具体的类型。
const aLink = document.getElementById('link')
console.log(aLink.href) // 这里会报错,因为返回的元素是HTMLElement,而不是a标签
这时我们就需要显式地指定该方法的返回值类型,以便于使用TS原本识别不出的属性。
const aLink = document.getElementById('link') as HTMLAnchorElement
console.log(aLink.href) // 这里会报错,因为返回的元素是HTMLElement,而不是a标签
泛型
泛型是可以在保证类型安全前提下,让函数等与多种类型一起工作,从而实现复用,常用于:函数、接口、class 中。
比如我们创建一个 id 函数,传入什么数据就返回该数据本身(也就是说,参数和返回值类型相同)。比如,id(10) 调用以上函数就会直接返回 10 本身。但是,该函数只接收数值类型,无法用于其他类型。
为了能让函数能够接受任意类型,可以将参数类型修改为 any。但是,这样就失去了 TS 的类型保护,类型不安全。
泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用。
实际上,在 C# 和 Java 等编程语言中,泛型都是用来实现可复用组件功能的主要工具之一。
泛型函数
定义泛型函数,可从参数类型和返回值类型处定义。
function id<Type>(value: Type): Type { return value }
function id<T>(value: T): T { return value }
在函数名称的后面添加 <> (尖括号),尖括号中添加类型变量,比如此处的 Type。类型变量Type是一种特殊类型的变量,它处理类型而不是值。
该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)。
因为 Type 是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型。
类型变量 Type,可以是任意合法的变量名称。
调用泛型函数:
const num = id<number>(10)
const str = id<string>('a')
当传入类型 number 后,这个类型就会被函数声明时指定的类型变量 Type 捕获。此时,Type 的类型就是 number,所以,函数 id 参数和返回值的类型也都是 number。同样,如果传入类型 string,函数 id 参数和返回值的类型就都是 string 。这样,通过泛型就做到了让 id 函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全。
在调用时我们也可以简化泛型函数调用,直接调用函数即可。
// 省略 <number> 调用函数
let num = id(10)
let str = id('a')
在调用泛型函数时,可以省略 <类型> 来简化泛型函数的调用。TS 内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量
Type 的类型。比如,传入实参 10,TS 会自动推断出变量 num 的类型 number,并作为 Type 的类型。推荐使用这种简化的方式调用泛型函数,使代码更短,更易于阅读。但当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数。
更具体的泛型
默认情况下,泛型函数的类型变量 Type 可以代表多个类型,这导致无法访问任何属性。比如,id('a') 调用函数时获取参数的长度:
function id<Type>(value: Type): Type {
console.log(value.length)
return value
}
id('a')
Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length。此,就需要为泛型添加约束来收缩类型 (缩窄类型取值范围)。添加泛型约束收缩类型,主要有以下两种方式:指定更加具体的类型、添加约束。
比如,将类型修改为 Type[] (Type 类型的数组),因为只要是数组就一定存在 length 属性,因此就可以访问了。
function id<Type>(value: Type[]): Type[] {
console.log(value.length)
return value
}
为泛型添加更具体的约束
我们可以通过让泛型继承自接口的方式指定泛型对应的类型有哪些行为是必须的。
// 创建一个接口
interface Length {
length: number
}
// Type extends Length 添加泛型约束
// 解释:表示传入的 类型 必须满足 Length 接口的要求才行,也就是得有一个 number 类型的 length 属性
function id<Type extends Length>(value: Type): Type {
console.log(value.length)
return value
}
创建描述约束的接口 Length,该接口要求提供 length 属性。通过 extends 关键字使用该接口,为泛型(类型变量)添加约束。该约束表示:传入的类型必须具有 length 属性。
注意:传入的实参(比如数组)只要有 length 属性即可(类型兼容性)。
多个泛型
泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束)。
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
let person = {name: 'jack', age: 18}
getProp(person, 'name')
- 添加了第二个类型变量 Key,两个类型变量之间使用 , 逗号分隔。
- keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型。
- 本示例中 keyof Type 实际上获取的是 person 对象所有键的联合类型,也就是:
'name' | 'age'
- 类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性。
// Type extends object 表示: Type 应该是一个对象类型,如果不是 对象 类型,就会报错
// 如果要用到 对象 类型,应该用 object ,而不是 Object
function getProperty<Type extends object, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
泛型接口
接口也可以配合泛型来使用,以增加其灵活性,增强其复用性。
interface IdFunc<Type> {
id: (value: Type) => Type
ids: () => Type[]
}
let obj: IdFunc<number> = {
id(value) {
return value
},
ids() {
return [1, 3, 5]
}
}
在接口名称的后面添加 <类型变量> ,那么,这个接口就变成了泛型接口。
接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量。
使用泛型接口时,需要显式指定具体的类型(比如,此处的 IdFunc)。
此时,id 方法的参数和返回值类型都是 number;ids 方法的返回值类型是 number[]。
JS中的泛型接口
实际上,JS 中的数组在 TS 中就是一个泛型接口。
const strs = ['a', 'b', 'c']
// 鼠标放在 forEach 上查看类型
strs.forEach // Array<string>
const nums = [1, 3, 5]
// 鼠标放在 forEach 上查看类型
nums.forEach // Array<number>
当我们在使用数组时,TS 会根据数组的不同类型,来自动将类型变量设置为相应的类型。
TypeScript与Vue
defineProps与Typescript
defineProps配合vue默认语法进行类型校验(运行时声明)。
// 运行时声明
defineProps({
money: {
type: Number,
required: true
},
car: {
type: String,
required: true
}
})
defineProps配合ts的泛型定义props类型校验,这样更直接。
// 使用ts的泛型指令props类型
defineProps<{
money: number
car?: string
}>()
props可以通过解构来指定默认值。
// 使用ts的泛型指令props类型
const { money, car = '小黄车' } = defineProps<{
money: number
car?: string
}>()
如果提供的默认值需要在模板中渲染,需要额外添加配置。
// vite.config.ts
export default {
plugins: [
vue({
reactivityTransform: true
})
]
}
defineEmits与Typescript
defineEmits配合运行时声明:
const emit = defineEmits(['change', 'update'])
defineEmits配合ts 类型声明,可以实现更细粒度的校验:
const emit = defineEmits<{
(e: 'changeMoney', money: number): void
(e: 'changeCar', car: string): void
}>()
ref与Typescript
通过泛型指定value的值类型,如果是简单值,该类型可以省略:
const money = ref<number>(10)
const money = ref(10)
如果是复杂类型,推荐指定泛型:
type Todo = {
id: number
name: string
done: boolean
}
const list = ref<Todo[]>([])
setTimeout(() => {
list.value = [
{id: 1, name: '吃饭', done: false},
{id: 2, name: '睡觉', done: true}
]
})
computed与Typescript
通过泛型可以指定computed计算属性的类型,通常可以省略:
const leftCount = computed<number>(() => {
return list.value.filter((item) => item.done).length
})
console.log(leftCount.value)
事件处理与Typescript
<script>
const move = (e: MouseEvent) => {
mouse.value.x = e.pageX
mouse.value.y = e.pageY
}
</script>
<template>
<h1 @mousemove = "move($event)" > 根组件 </h1>
</template>
Template Ref与Typescript
const imgRef = ref<HTMLImageElement | null>(null)
onMounted(() => {
console.log(imgRef.value?.src)
})
如何查看一个DOM对象的类型:通过控制台进行查看。
document.createElement('img').__proto__
可选链操作符
可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。
let nestedProp = obj.first?.second;
console.log(res.data?.data)
obj.fn?.()
if (obj.fn) {
obj.fn()
}
obj.fn && obj.fn()
// 等价于
let temp = obj.first;
let nestedProp = ((temp === null || temp === undefined) ? undefined : temp.second);
非空断言
如果我们明确的知道对象的属性一定不会为空,那么可以使用非空断言 !
。
// 告诉typescript, 明确的指定obj不可能为空
let nestedProp = obj!.second;
非空断言一定要确保有该属性才能使用,不然使用非空断言会导致bug。
其他
TypeScript类型声明文件
TS有两种文件类型,分别为.ts和.d.ts。
- .ts:既包含类型信息也包含可执行代码、可以被编译成.js文件,然后执行代码。
- .d.ts:只包含类型信息的类型声明文件,不会生成.js文件,仅用于提供类型信息,且.d.ts文件内不允许出现可执行代码。