TOC
CHAT

TypeScript基础教程

TypeScript(TS)是微软开发的开源编程语言,是JavaScript的超集,在JS基础之上添加了类型支持。相比JS具有如下优势:

  1. 更早(写代码的同时)发现错误,减少找bug、改bug时间,提升开发效率。
  2. 程序中任何位置的代码都有代码提示,随时随地的安全感,增强了开发体验。
  3. 强大的类型系统提升了代码的可维护性,使得重构代码更加容易。
  4. 支持最新的ECMAScript语法,优先体验最新的语法,走在前端技术的最前沿。
  5. 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代码

基本步骤如下:

  1. 编写hello.ts文件(TS文件的后缀为.ts
  2. 将TS编译为JS:tsc hello.ts(此时在同级目录中会出现一个同名的.js文件)
  3. 执行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中的常用基础类型细分为两类:

  1. JS已有类型
    • 原始类型:number、string、boolean、null、undefined、symbol
    • 对象类型:object(包括数组、对象、函数等)
  2. 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

为函数指定类型的两种方式:

  1. 单独指定参数、返回值的类型
function add(num1: number, num2: number): number {
    return num1 + num2
}
const add = (num1: number, num2: number): number => {
    return num1 + num2
}
  1. 同时指定参数、返回值的类型
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中的高级类型有很多,重点掌握以下高级类型:

  1. 类(class)
  2. 类型兼容性
  3. 交叉类型
  4. 泛型、keyof
  5. 索引签名类型、索引查询类型
  6. 映射类型

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中的可见性修饰符如下所示:

  1. public:公有成员,可以被任何地方访问【默认】
  2. protected:仅对其声明所在类和子类中(非实例对象)可见
  3. private:只在当前类中可见,对实例对象及子类均不可见
  4. 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()

函数兼容性比较复杂,需要考虑参数个数、参数类型、返回值类型:

  1. 参数个数:参数多的兼容参数少的(参数少的可以赋值给多的
    • 注意与成员个数的兼容规则相反
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会自动推导出参数itemindexarray的类型。

  1. 参数类型:相同位置的参数类型相同(原始类型)或兼容(对象类型)
    • 注意与接口兼容性相反
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 // 注意与前述的接口兼容性相反
  1. 返回值类型:只关注返回值类型本身,故对于对象类型的返回值详见前述的成员个数兼容规则
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可以代表多个类型,这导致无法访问任何属性,此时就需要为泛型添加约束来收缩类型(缩窄类型取值范围)。主要有以下两种方式——

  1. 指定更加具体的类型
function id<Type>(value: Type[]): Type[] {    // 设置类型为Type[]
    console.log(value.length)
    return value
}
  1. 添加约束
    • 原理:接口兼容性
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中的一些常见操作。常用的有如下几种——

  1. Partial<Type>:用于构造一个类型,将Type的所有属性设置为可选
interface Props {
    id: string
    children: number[]
}
type PartialProps = Patial<Props>
  1. Readonly<Type>:用于构造一个类型,将Type的所有属性都设置为readonly(只读)
interface Props {
    id: string
    children: number[]
}
type ReadonlyProps = Readonly<Props>
  1. Pick<Type, Keys>:从Type中选择一组属性(Keys)来构造新类型,其中传入的Keys只能是Type中存在的属性
interface Props {
    id: string
    title: string
    children: number[]
}
type PickProps = Pick<Props, 'id' | 'title'>
  1. 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}

注意:映射类型只能在类型别名中使用,不能在接口中使用。

映射类型除了根据联合类型创建新类型外,还可以根据对象类型来创建,需要使用keyofs 获取对象类型中所有键的联合类型:

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都提供了声明文件。

第三方库的类型声明文件:目前几乎所有常用的第三方库都有相应的类型声明文件,有如下两种存在形式——

  1. 库自带类型声明文件:例如axios。这种情况下,正常导入该库后TS就会自动加载库自带类型声明文件,以提供该库的类型声明。
  2. 由DefinitelyTyped提供:DefinitelyTyped(GitHub仓库:DefinitelyTyped)提供了高质量TypeScript类型声明,可通过npm/yarn来下载该仓库提供的TS类型声明包,包名格式统一为@types/*(例如@types/react@types/lodash等),有需要时下载即可。

4.2 创建自己的类型声明文件

通常有如下两种情形:

  1. 项目内共享类型:如果多个.ts文件中都用到同一个类型,此时可以创建.d.ts文件提供该类型,实现类型共享。
    • 操作步骤:
      1. 创建index.d.ts类型声明文件
      2. 创建需要共享的类型,并使用export导出(TS中的类型也可以使用import/export实现模块化功能)
      3. 在需要使用共享类型的.ts文件中,通过import导入即可(可直接省略.d.ts后缀)
  2. 为已有JS文件提供类型声明:在将JS项目迁移到TS项目时,为了让已有的.js文件有类型声明;或者成为库作者,创建库供其他人使用。
    • 类型声明文件的编写与模块化方式相关,不同的模块化方式有不同的写法。
    • TS项目中也可以使用.js文件,在导入.js文件时,TS会自动加载与.js同名的.d.ts文件,以提供类型声明。
    • declare关键字:用于类型声明,为其他地方(例如.js文件)已存在的变量声明类型,而不是创建一个新的变量。规则如下——
      1. 对于typeinterface等明确是TS类型的(只能在TS中使用的),可以省略declare关键字。
      2. 对于letfunction等具有双重含义的(在JS、TS中均可用),须使用declare关键字,明确指定此处用于类型声明。

5 在React中使用TypeScript

5.1 使用CRA创建支持TS的项目

React脚手架工具create-react-appCRA)默认支持 TypeScript。

创建支持TS的项目命令:npx create-react-app 项目名称 --template typescript

相对于非TS项目,目录结构主要由以下三个变化:

  1. 项目根目录中增加了tsconfig.json配置文件,用于指定TS的编译选项(例如编译时是否移除注释)
  2. React组件的文件扩展名变为*.tsx
  3. src目录中增加了react-app-env.d.ts,为React项目默认的类型声明文件

三斜线指令:指定依赖的其他类型声明文件,types表示依赖的类型声明文件包的名称。

/// <reference types="react-scripts" />

如上所示,加载react-scripts包提供的类型声明(实际上TS会自动加载该.d.ts文件,以提供类型声明),其包含如下两部分类型:

  1. react、react-dom、node的类型
  2. 图片、样式等模块的类型,以允许在代码中导入图片、SVG等文件

5.2 TS配置文件tsconfig.json

tsconfig.json用于指定项目文件和项目编译所需的配置项,位于项目根目录(与package.json同级),可以使用命令tsc --init自动生成该文件。

除了在tsconfig.json文件中使用编译配置外,还可通过命令行来使用,例如tsc hello.ts --target es6

注意当tsc后不带输入文件(例如tsc)时才会启用tsconfig.json,推荐使用该文件。

发表评论