TypeScript(TS)是微软开发的开源编程语言,是JavaScript的超集,在JS基础之上添加了类型支持。相比JS具有如下优势:
- 更早(写代码的同时)发现错误,减少找bug、改bug时间,提升开发效率。
- 程序中任何位置的代码都有代码提示,随时随地的安全感,增强了开发体验。
- 强大的类型系统提升了代码的可维护性,使得重构代码更加容易。
- 支持最新的ECMAScript语法,优先体验最新的语法,走在前端技术的最前沿。
- TS类型推断机制,不需要在代码中的每个地方都显示标注类型,使得在享受优势的同时,尽量降低了成本。
除此之外,Vue3源码使用TS重写、Angular默认支持TS、React与TS完美配合,TypeScript已成为大中型前端项目的首选编程语言。
1 TS快速入门
TypeScript作为JavaScript的超集(JS有的TS都有),可以在任何运行JavaScript的地方运行。
// TS代码
let age1: number = 18
// JS代码
let age2 = 18
1.1 为何要为JS添加类型支持
JS的类型系统存在“先天缺陷”,JS代码中绝大部分错误都是类型错误(Uncaught TypeError),这增加了找bug、改bug的时间,严重影响开发效率。
从编程语言的动静来区分,TS属于静态类型(编译期做类型检查)的编程语言,JS属于动态类型(执行期做类型检查)的编程语言。
对于JS来说,需要等到代码真正去执行的时候才能发现错误;对于TS来说:在代码编译的时候(代码执行前)就可以发现错误,并且配合各种IDE,TS可以提前到在编写代码的同时就发现代码中的错误,减少找bug、改bug的时间。
1.2 安装编译TS的工具包
Node.js和浏览器需要先将TS代码编译为JS代码,然后才能运行。typescript包提供了tsc
编译命令,实现了TS到JS的转化。
- 安装命令:
npm i -g typescript
- 验证是否安装成功:
tsc -v
(查看typescript的版本)
1.3 编译并运行TS代码
基本步骤如下:
- 编写
hello.ts
文件(TS文件的后缀为.ts
) - 将TS编译为JS:
tsc hello.ts
(此时在同级目录中会出现一个同名的.js
文件) - 执行JS代码:
node hello.js
每次修改代码后,都要重复执行上述两个命令才能运行TS代码,太繁琐。可以使用ts-node
包直接在Node.js中执行TS代码。
- 安装命令:
npm i-g ts-node
- 使用方式:
ts-node hello.ts
原理:ts-node
命令在内部将TS编译成JS,然后再运行JS代码。
注意:若ts-node hello.ts
执行报错,需要先执行命令tsc --init
创建一个tsconfig.json
文件。该文件是TypeScript项目的配置文件,包含TypeScript编译的相关配置。通过更改编译配置项,可以让TypeScript编译出ES6、ES5、node等的代码。
2 常用类型
TS提供了JS的所有功能,并且额外增加了类型系统。
实际上JS有类型(number、string等),但动态类型的JS不会检查变量的类型是否发生变化,而静态类型的TS会检查。
let age: number = 18
如上所示的: number
即为类型注解,用于为变量添加类型约束。
可以将TS中的常用基础类型细分为两类:
- JS已有类型
- 原始类型:number、string、boolean、null、undefined、symbol
- 对象类型:object(包括数组、对象、函数等)
- TS新增类型
- 联合类型、自定义类型(类型别名)、接口、元组、字面量类型、枚举、void、any 等.
2.1 原始类型
原始类型:number、string、boolean、null、undefined、symbol
完全按照JS中类型的名称来书写即可。
let age: number = 137
let myName: string = 'Akira'
let isLoading: boolean = false
2.2 数组类型
JS中的对象类型(Object)在TS中更加细化每个具体的对象都有自己的类型语法。
数组类型的两种写法:元素类型后添加[]
、使用泛型Array<>
(推荐前者)
let numbers: number[] = [1, 3, 5]
let strings: Array<string> = ['a', 'b', 'c']
2.3 联合类型
联合类型:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种,类型之间用|
(单竖线)分隔
let arr: (number | string)[] = [1, 'a', 3, 'b']
如上所示的数组中既有number类型,又有string类型。
2.4 类型别名
类型别名(自定义类型):当同一类型被多次使用时,可以通过类型别名简化该类型的使用。
type CustomArray = (number | string)[]
let arr1: CustomArray = ['x', 'y', 6, 7]
如上所示,使用type
关键字来创建别名。
2.5 函数类型
函数的类型实际指的是函数参数和返回值的类型。如果函数没有返回值,那么函数返回值类型为void
。
为函数指定类型的两种方式:
- 单独指定参数、返回值的类型
function add(num1: number, num2: number): number {
return num1 + num2
}
const add = (num1: number, num2: number): number => {
return num1 + num2
}
- 同时指定参数、返回值的类型
const add: (num1: number, num2: number) => number = (num1, num2) => {
return num1 + num2
}
可选参数:在可传可不传的参数名称后添加?
,只能出现在参数列表的最后
function mySlice(start?: number, end?: number): void {
console.log('起始索引:', start, '结束索引:', end)
}
2.6 对象类型
JS中的对象由属性和方法构成,TS中的对象类型即用于描述对象的结构(有什么类型的属性和方法)。
直接使用{}
来描述对象结构,属性采用属性名: 类型
的形式;方法可采用方法名([参数类型]): 返回值类型
的形式,或使用箭头函数形式,例如{ sayHi: (name: string) => void }
。。
在一行代码中指定对象的多个属性类型时,使用;
(分号)来分隔(若一行代码中仅定义1个属性,则无需使用分号,后述的接口定义同理)。
let person: { name: string; age: number; sayHi(name: string): void } = {
name: 'Akira',
age: 137,
sayHi() {
console.log(name)
}
}
亦可使用可选属性,参考函数可选参数使用?
即可。
function myAxios(config: {url: string, method?: string}) {
console.log(config)
}
2.7 接口
当一个对象类型被多次使用时,可以使用接口(Interface)来描述对象的类型,达到复用的目的。
使用interface
关键字来声明接口,声明后可直接使用接口名称作为变量的类型。
interface IPerson {
name: string
age: number
sayHi(): void
}
let person: IPerson = {
name: 'Akira',
age: 137,
sayHi() {}
}
辨析interface(接口)与type(类型别名):接口只能为对象指定类型;类型别名不仅可以为对象指定类型,实际上可以为任意类型指定别名。
接口的继承使用extends
关键字,如下例所示:
interface Point2D { x: number; y: number }
interface Point3D extends Point2D { z: number }
2.8 元组
元组类型是特殊的数组,可以确切地标记出有多少个元素,以及每个元素的类型。除了精准记录元素对外,还常应用于接收多个返回参数、变量值交换等情形。
let position: [number, number]: [39.5427, 116.2317]
2.9 类型推论
在TS中某些未指明类型的地方,TS的类型推论机制会帮助提供类型。
let age = 18 // 自动推断出number类型
推荐:能省略类型注解的地方就省略
2.10 类型断言
可以使用类型断言来指定更具体的类型。
【例】读取<a>
标签
<a href="https://www.hyperplasma.top" id="link">Hyplus</a>
const aLink = document.getElementById('link') // 类型推论为HTMLElement
getElementByid
方法返回值的类型为HTMLElement,该类型只包含所有标签公共的属性或方法,不包含<a>
标签特有的href
等属性。因此这个类型太宽泛(不具体),无法操作特有属性或方法。此时就需要使用类型断言指定更加具体的类型,语法如下(其中HTMLAnchorElement是HTMLElementd 子类):
const aLink = document.getElementById('link') as HTMLAnchorElement
或者(了解即可)
const aLink = <HTMLAnchorElement>document.getElementById('link')
技巧:在浏览器控制台通过
console.dir()
打印DOM元素,在属性列表最后面即可看到该元素的类型。
2.11 字面量类型
任意的JS字面量(number、string、对象等)都可以作为类型使用。通常配合联合类型一起使用,用来表示一组明确的可选值列表。
function changeDirection(direction: 'up' | 'down' | 'left'| 'right') {
console.log(direction)
}
优势:相比于直接使用string类型,使用字面量类型更加精确、严谨。
2.12 枚举
枚举类似于字面量类型+联合类型组合,表示一组明确的可选值。使用enum
关键字定义一组命名常量来描述一个值,该值可以是这些命名常量中的一个。
使用方式如下例所示:
enum Direction { Up, Down, Left, Right }
function changeDirection(direction: Direction) {
console.log(direction)
}
changeDirection(Direction.Up)
枚举成员是有值的,默认为从0开始自增的数值,这种枚举成员的值为数字的枚举称为数字枚举。亦可给枚举成员手动初始化值(参考C语言):
enum Direction { Up = 10, Down, Left, Right } // Down = 11, Left = 12, Right = 13
enum Direction2 { Up = 2, Down = 4, Left = 8, Right = 16 }
枚举成员的值是字符串的枚举称为字符串枚举。注意字符串枚举没有自增长行为,因此每个成员必须有初始值。
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
枚举是TS为数不多的非JavaScript类型级扩展(不仅仅是类型)的特性之一,因为其他类型仅仅被当做类型,而枚举不仅用作类型,还提供值。即其他类型会在编译为JS代码时自动移除,但是枚举类型会被编译为JS代码。
例如上述字符串枚举最终会被编译为如下代码:
var Direction;
(function (Direction) {
Direction["Up"] = "UP";
Direction["Down"] = "DOWN";
Direction["Left"] = "LEFT";
Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
2.13 any类型
不推荐使用any
!这会让TypeScript变为 “AnyScript”(失去TS类型保护的优势),因为当值的类型为any时,可以对该值进行任意操作,并且不会有代码提示。
如下例所示的所有操作都不会有任何类型错误提示,即使可能存在错误:
let obj: any = { x: 0}
obj.bar = 100
obj()
const n: number = obj
其他隐式具有any类型的情况:声明变量不提供类型也不提供默认值;函数参数不加类型。
尽可能的避免使用any,除非临时使用any来“避免”书写极为复杂的类型!
2.14 typeof
JS中的typeof
操作符可用于获取数据的类型,在TS中还可以在类型上下文(类型注解的位置)中引用变量或属性的类型(类型查询),如下例所示:
let p = { x: 1, y: 2 }
function formatPoint(point: typeof p) {} // 等价于 function formatPoint(point: { x: number; y: number }) {}
formatPoint(p)
注意:typeof
只能用于查询变量或属性的类型,无法查询其他形式的类型(比如函数调用的类型)
3 高级类型
TS中的高级类型有很多,重点掌握以下高级类型:
- 类(class)
- 类型兼容性
- 交叉类型
- 泛型、keyof
- 索引签名类型、索引查询类型
- 映射类型
3.1 类(class)
TypeScript全面支持ES2015中引入的class
关键字,并添加了类型注解和其他语法(例如可见性修饰符等)
TS中的class不仅提供了class的语法功能,也作为一种类型存在。
class Person {
age: number
gender = 'male' // 可设置初始值,此时会自动进行类型推断
}
const p = new Person() // 类型推论得p的类型为Person
构造函数与实例方法:
class Person {
age: number
gender: string
// 构造函数:使用constructor关键字,无需指定返回值类型
constructor(age: number, gender: string) {
this.age = age
this.gender = gender
}
// 实例方法
printAge(): void {
console.log(this.age)
}
}
使用关键字extends
继承父类(源自JS):
class Animal {
move() { console.log('Moving along!') }
}
class Dog extends Animal {
bark() { console.log('Bark!') }
}
const dog = new Dog()
使用关键字implements
实现接口(TS特有),实现类中必须提供接口中指定的所有属性和方法:
interface Singable {
sing(): void
}
class Person implements Singable {
sing() {
console.log('Hyper~ Plasma~')
}
}
类成员可见性表示类的方法或属性对于class外的代码是否可见。TS中的可见性修饰符如下所示:
public
:公有成员,可以被任何地方访问【默认】protected
:仅对其声明所在类和子类中(非实例对象)可见private
:只在当前类中可见,对实例对象及子类均不可见readonly
:表示只读,防止在构造函数之外对属性进行赋值;只能修饰属性不能修饰方法;接口或{ }
表示的对象类型也可以使用readonly
3.2 类型兼容性
存在两种类型系统:Structural Type System(结构化类型系统)、Nominal Type System(标明类型系统)
TS采用结构化类型系统(又称为Duck Typing,鸭子类型),类型检查关注的是值所具有的形状,即在结构类型系统中,y的成员至少与x相同,则x兼容y(成员多的可以赋给少的)。如下例所示,注意与c#、java等标明类型系统语言的重大区别:
class Point { x: number; y: number }
class Point3D { x: number; y: number; z: number }
const p: Point = new Point3D()
除了class之外,TS中的其他类型也存在相互兼容的情况。
接口兼容性类似于class,并且class和interface之间也可以兼容:
interface Point { x: number; y: number }
interface Point2D { x: number; y: number }
let p1: Point
let p2: Point2D = p1
interface Point3D { x: number; y: number; z: number }
let p3: Point3D
p2 = p3
class Point3DImpl { x: number; y: number; z: number }
let p2Impl: Point2D = new Point3DImpl()
函数兼容性比较复杂,需要考虑参数个数、参数类型、返回值类型:
- 参数个数:参数多的兼容参数少的(参数少的可以赋值给多的)
- 注意与成员个数的兼容规则相反
type F1 = (a: number) => void
type F2 = (a: number, b: number) => void
let f1: F1
let f2: F2 = f1
const arr = ['a', 'b', 'c']
arr.forEach(() => {})
arr.forEach((item) => {}) // 等价于上一行用法
如上所示,数组
forEach
方法的第一个参数是回调函数,该示例中类型为:(value: string, index: number, array: string[]) => void
。
在JS中省略用不到的函数参数实际上是很常见的,这样的使用方式促成了TS中函数类型之间的兼容性。
并且因为回调函数是有类型的,所以TS会自动推导出参数item
、index
、array
的类型。
- 参数类型:相同位置的参数类型相同(原始类型)或兼容(对象类型)
- 注意与接口兼容性相反
type F1 = (a: number) => void
type F2 = (a: number) => void
let f1: F1
let f2: F2 = f1
interface Point2D { x: number; y: number }
interface Point3D { x: number; y: number; z: number }
type F3 = (p: Point2D) => void
type F4 = (p: Point3D) => void
let f3: F3
let f4: F4 = f3 // 注意与前述的接口兼容性相反
- 返回值类型:只关注返回值类型本身,故对于对象类型的返回值详见前述的成员个数兼容规则
type F5 = () => string
type F6 = () => string
let f5: F5
let f6: F6 = f5
type F7 = () => { name: string }
type F8 = () => { name: string; age: number }
let f7: F7
let f8: F8
f7 = f8
3.3 交叉类型
交叉类型(&
)类似于继承,用于组合多个类型为一个类型,新的类型同时具备所组合类型的所有属性类型。
interface Person { name: string }
interface Contact { phone: string }
type PersonDetail = Person & Contact // 等价于 type PersonDetail = { name: string; phone: string }
let obj: PersonDetail = {
name: 'Akira',
phone: '12345678901'
}
交叉类型(&
)和继承(extends
)的对比:
- 相同点:都可以实现对象类型的组合。
- 不同点:两种方式实现类型组合时,同名属性之间处理类型冲突的方式不同。如下所示——
interface A {
fn: (value: number) => string
}
/* 以下代码会报错
interface B extends A {
fn: (value: string) => string
}
*/
interface A {
fn: (value: number) => string
}
interface B {
fn: (value: string) => string
}
type C = A & B // 无错误。相当于将函数fn转化为 fn: (value: string | number) => string
3.4 泛型、keyof
泛型在保证类型安全前提下,可以让函数等与多种类型一起工作,灵活可复用,常用于函数、接口、class中。
创建并调用泛型函数示例:
function id<Type>(value: Type): Type { return value }
const num = id(10) // 可省略调用时函数名后的`<>`
const str = id('a')
泛型约束:默认情况下,泛型函数的类型变量Type
可以代表多个类型,这导致无法访问任何属性,此时就需要为泛型添加约束来收缩类型(缩窄类型取值范围)。主要有以下两种方式——
- 指定更加具体的类型
function id<Type>(value: Type[]): Type[] { // 设置类型为Type[]
console.log(value.length)
return value
}
- 添加约束
- 原理:接口兼容性
interface ILength { length: number }
function id<Type extends ILength>(value: Type): Type {
console.log(value.length)
return value
}
泛型的类型变量可以有多个,并且类型变量之间还可以约束:使用keyof
关键字接收一个对象类型,生成其键名称的联合类型(字符串或数字)。如下例所示:
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
let person = { name: 'Akira', age: 137 }
getProp(person, 'name') // 此时泛型Key相当于 'name' | 'age'
泛型接口:
interface IdFunc<Type> {
id: (value: Type) => Type
ids: () => Type[]
}
let obj: IdFunc<number> = { // 必须显式指定具体的类型
id(value) { return value },
ids() { return [1, 3, 5] }
}
实际上,JS中的数组在TS中就是一个泛型接口(
Array<Type>
)。
泛型类:
class GenericNumber<NumType> {
defaultValue: NumType
add: (x: NumType, y: NumType) => NumType
}
const myNum = new GenericNumber<number>()
myNum.defaultValue = 10
泛型工具类型:TS内置了一些常用的工具类型,来简化TS中的一些常见操作。常用的有如下几种——
Partial<Type>
:用于构造一个类型,将Type
的所有属性设置为可选
interface Props {
id: string
children: number[]
}
type PartialProps = Patial<Props>
Readonly<Type>
:用于构造一个类型,将Type
的所有属性都设置为readonly
(只读)
interface Props {
id: string
children: number[]
}
type ReadonlyProps = Readonly<Props>
Pick<Type, Keys>
:从Type
中选择一组属性(Keys
)来构造新类型,其中传入的Keys
只能是Type
中存在的属性
interface Props {
id: string
title: string
children: number[]
}
type PickProps = Pick<Props, 'id' | 'title'>
Record<Keys, Type>
:构造一个对象类型,属性键为Keys
,属性类型为Type
type RecordObj = Record<'a' | 'b' | 'c', string[]>
let obj: RecordObj = {
a: ['1'],
b: ['2'],
c: ['3']
}
3.5 索引签名类型
当无法确定对象中有哪些属性(或者说对象中可以出现任意多个属性)时,可以使用索引签名类型。
在JS中对象({ }
)的键类型为string,对应于索引签名类型为string,语法表示为[key: string]
:
interface AnyObject {
[key: string]: number
}
let obj: AnyObject = {
a: 2,
abc: 1,
abcd: 1245
}
obj['a'] = 1020
obj['abc'] = -20
在JS中数组是一类特殊的对象,数组的键(索引)是数值类型,并且数组中元素个数可以为任意个,故在数组对应的泛型接口中,也用到了索引签名类型。
如下所示的接口模拟原生的数组接口,其中索引签名类型为[n: number]
,表示只要是number类型的键(索引)都可以出现在数组中,或者说数组中可以有任意多个元素。同时也符合数组索引是number类型这一前提。
interface MyArray<T> {
[n: number]: T
}
let arr: MyArray<number> = [1, 3, 5]
3.6 映射类型、索引查询类型
映射类型:基于旧类型创建新类型(对象类型),减少重复、提升开发效率。
映射类型基于索引签名类型,语法为[Key in PropKeys]
。其中Key in PropKeys
表示Key
可以是PropKeys
联合类型中的任意一个,类似于for-in。如下例所示——
type PropKeys = 'x' | 'y' | 'z'
type Type = { [Key in PropKeys]: number } // 等价于 { x: number; y: number; z: number}
注意:映射类型只能在类型别名中使用,不能在接口中使用。
映射类型除了根据联合类型创建新类型外,还可以根据对象类型来创建,需要使用keyof
s 获取对象类型中所有键的联合类型:
type Props = { a: number; b: string; c: boolean }
type Type = { [key in keyof Props]: number }
实际上前述的泛型工具类型都是基于映射类型实现的,例如Partial<Type>
的内部实现如下:
type Partial<T> = {
[P in keyof T]?: T[P] // `?`用于实现可选;T[P]表示获取T中每个键(P)对应的类型
}
type Props = { a: number; b: string; c: boolean }
type PartialProps = Partial<Props>
上述中的T[P]
为索引查询(访问)类型,用于查询属性的类型([]
内的属性必须存在被查询类型中),可以同时查询1个或多个。如下所示:
type Props = { a: number; b: string; c: boolean }
type TypeA = Props['a'] // number
type TypeB = Props['a' | 'b'] // string | number
type TypeC = Props[keyof Props] // string | number | boolean
4 类型声明文件
TS中存在两种文件类型:
.ts
文件:包含类型信息和可执行代码- 可以被编译为
.js
文件,然后执行代码 - 用途:编写程序代码
- 可以被编译为
.d.ts
文件:只包含类型信息的类型声明文件- 不会生成
.js
文件,仅用于提供类型信息 - 用途:为JS提供类型信息
- 不会生成
4.1 使用已有的类型声明文件
内置类型声明文件:TS为JS运行时可用的所有标准化内置API都提供了声明文件。
第三方库的类型声明文件:目前几乎所有常用的第三方库都有相应的类型声明文件,有如下两种存在形式——
- 库自带类型声明文件:例如axios。这种情况下,正常导入该库后TS就会自动加载库自带类型声明文件,以提供该库的类型声明。
- 由DefinitelyTyped提供:DefinitelyTyped(GitHub仓库:DefinitelyTyped)提供了高质量TypeScript类型声明,可通过npm/yarn来下载该仓库提供的TS类型声明包,包名格式统一为
@types/*
(例如@types/react
、@types/lodash
等),有需要时下载即可。
4.2 创建自己的类型声明文件
通常有如下两种情形:
- 项目内共享类型:如果多个
.ts
文件中都用到同一个类型,此时可以创建.d.ts
文件提供该类型,实现类型共享。- 操作步骤:
- 创建
index.d.ts
类型声明文件 - 创建需要共享的类型,并使用
export
导出(TS中的类型也可以使用import/export实现模块化功能) - 在需要使用共享类型的
.ts
文件中,通过import
导入即可(可直接省略.d.ts
后缀)
- 创建
- 操作步骤:
- 为已有JS文件提供类型声明:在将JS项目迁移到TS项目时,为了让已有的
.js
文件有类型声明;或者成为库作者,创建库供其他人使用。- 类型声明文件的编写与模块化方式相关,不同的模块化方式有不同的写法。
- TS项目中也可以使用
.js
文件,在导入.js
文件时,TS会自动加载与.js
同名的.d.ts
文件,以提供类型声明。 declare
关键字:用于类型声明,为其他地方(例如.js
文件)已存在的变量声明类型,而不是创建一个新的变量。规则如下——- 对于
type
、interface
等明确是TS类型的(只能在TS中使用的),可以省略declare
关键字。 - 对于
let
、function
等具有双重含义的(在JS、TS中均可用),须使用declare
关键字,明确指定此处用于类型声明。
- 对于
5 在React中使用TypeScript
5.1 使用CRA创建支持TS的项目
React脚手架工具create-react-app
(CRA)默认支持 TypeScript。
创建支持TS的项目命令:npx create-react-app 项目名称 --template typescript
相对于非TS项目,目录结构主要由以下三个变化:
- 项目根目录中增加了
tsconfig.json
配置文件,用于指定TS的编译选项(例如编译时是否移除注释) - React组件的文件扩展名变为
*.tsx
- src目录中增加了
react-app-env.d.ts
,为React项目默认的类型声明文件
三斜线指令:指定依赖的其他类型声明文件,types
表示依赖的类型声明文件包的名称。
/// <reference types="react-scripts" />
如上所示,加载react-scripts包提供的类型声明(实际上TS会自动加载该.d.ts
文件,以提供类型声明),其包含如下两部分类型:
- react、react-dom、node的类型
- 图片、样式等模块的类型,以允许在代码中导入图片、SVG等文件
5.2 TS配置文件tsconfig.json
tsconfig.json
用于指定项目文件和项目编译所需的配置项,位于项目根目录(与package.json
同级),可以使用命令tsc --init
自动生成该文件。
除了在tsconfig.json
文件中使用编译配置外,还可通过命令行来使用,例如tsc hello.ts --target es6
。
注意当tsc后不带输入文件(例如tsc
)时才会启用tsconfig.json
,推荐使用该文件。