万字长文✨TypeScript基础语法汇总✨进来看看吧~

64 阅读27分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

一、什么是TypeScript

TypeScript并不是一个全新的语言,准确来说它是JavaScript的超集

  • TypeScript包含了目前主流的ES5标准,同时包含了ES6、7、8和未来的一些标准
  • 甚至还包含了处于实验阶段的语法,比如装饰器等等...
  • TypeScript构建于JavaScript之上,同时为JavaScript带来了强类型的定义

需要注意的是,TypeScript无法在浏览器上运行,所以需要借助编译器将TypeScript编译为JavaScript

既然,使用TypeScript还需要编译,这么麻烦,为什么我们还需要使用呢?

这是由于TypeScript为JavaScript带来了强类型定义,强类型定义可以:

  • 规范我们的代码
  • 可以使我们在代码编译阶段就能及时发现错误
  • 可以理解为,TypeScript在原生的JS基础上加上了一层类型定义

二、为什么使用TypeScript

比如,现在有以下代码,期望计算两个数字之和

function add(num1, num2) {
  return num1 + num2
}
console.log(add('2', '3')) // 23

但是一旦传递字符串,计算出来的结果并不是预期结果,而这种错误并不是技术错误也不是语言级别的错误甚至连异常都不会抛出,但是它却是语义级别的逻辑错误。

类似于这种错误,在JavaScript中非常常见,为了避免这种错误,可配合if语句结合类型检查来避免。比如:

function add(num1, num2) {
  if (typeof num1 === 'number' && typeof num2 === 'number') {
    return num1 + num2
  }
  return +num1 + +num2
}

但是这样就造成了大量的工作量,需要做各种的类型判断,这就是TypeScript出现的原因,使用TypeScript可以:

  • 进行类型推演与类型匹配
  • 自动进行类型检查,在开发编译时及时报错
  • 避免低级错误、写出质量更高的代码
  • 解放劳动力,减少很多判断类型的逻辑性代码的编写

而如果使用TypeScript,可以写为如下代码:

function add(num1: number, num2: number) {
  return num1 + num2
}

三、TypeScript环境配置

TypeScript官网

点击官网上方的Docs选项

点击Get Started中的TypeScript Tooling in 5 minutes即可看到配置环境的步骤

  1. 全局安装TypeScript
npm i -g typescript
  1. 安装完后在命令行中输入tsc -v如果能打印出版本号便证明安装成功
  2. 需要注意的是TypeScript工具链的运行需要依赖node环境,所以要事先安装好node(node官网),最好安装LTS稳定版本。
  3. 现在便可以在编辑器中创建ts文件,然后用tsc编译后运行

  1. 编译后,可发现目录中多了一个同名的js文件,此时使用node xx.js便可运行该js文件

  1. 至此TypeScript环境搭建完毕

除了以上方式外,我们也可以安装ts-node

  • npm i -g ts-node
  • 它可以帮助我们直接运行ts文件

五、TypeScript类型

number数字类型

  • 对数字的定义只有一个number类型来表示
  • 既能表示整数、也能表示浮点数、甚至表示正负数
  • 比如 1、7.3、-98

在声明变量时使用

let decLiteral: number = 20 // 十进制
let hexLiteral: number = 0x14 // 十六进制
let binaryLiteral: number = 0b10100 // 二进制
let octalLiteral: number = 0o24 // 八进制

在函数参数上使用

function add(num1: number, num2: number) {
  return num1 + num2
}

在定义变量时如果为变量同时赋值了数值,TypeScript会根据上下文自动推导出变量的类型

string字符串类型

  • 字符串可以使用双引号、单引号、反引号来包裹
  • 比如 "hello" 、'hello'、`hello`
  • TypeScript 中也是如此
let name: string = 'kiki'
function sayHi(content: string) {
  console.log(content)
}
let str = '123' // ts此时会自动推导出str的类型为string

boolean布尔类型

let b:boolean = false
let a = true // ts此时会自动推导出a的类型为boolean

array数组类型

let list: number[] = [1,2,3]

let arr: Array<number> = [1,2,3]

let list2 = [1,2,3] // ts此时会自动推导出list2的类型为number[]

我们知道在JS中,数组中可以存放任何数据,那么在TS中可以通过以下方式实现

let list1 = [1, 'abc']
let list2: any[] = [1, 'abc']

一种方式是在声明的同时对数组进行初始化,第二种方式是在创建的时候显示声明类型为any[],此时可以发现:

第一种方式创建出来的数组中只能存放string / number类型的数据,可以发现向里面push进去boolean类型的数据会报错

而第二种方式创建出来的数组可以存放任意类型的数据

tuple元组类型

let list: [number, string] = [1, '123']

通过如上方式定义的元组类型,表示该数组一共有两个元素,且第一个元素为number类型,第二个元素为string类型

  • 可以将元组理解为固定长度、固定类型的特殊数组

可以发现,一旦去修改元素中的内容为其他类型则会报错,且不能更改数组的长度:

使用元组的好处是,我们可以很方便的定义出key、value形式的键值对来进行逻辑处理,比如数组中第一个元素表示学号,第二个元素表示姓名

需要注意的是!

  • 虽然不能通过数组[索引] = xx的方式来改变元组的长度
  • 但是如果向元组中push进去元组中所包含的类型的内容编译是可以通过的,所以使用元组时要注意这一点

还需要注意的是!

当通过let array = [1, '123']这种方式定义数组,ts会自动推导出来类型,这种类型是联合类型,而不是元组类型!

  • 联合类型与元组的区别是,联合类型不会限制数组的长度、而且不会限制数组各索引位置的数据类型,改变元素类型时只要包含在联合类型中都是可以编译成功的

union联合类型

用来表示一个变量可以同时支持多个类型

  • 支持的不同类型之间用"|"进行分隔

也可以表示一个变量可以是几个确定值中的某一个

  • 不同值之间用"|"进行分隔
let data: string | number
data = 123
data = '123'

let data2: 0 | 1 | 2 // 表示data2只能是0、1、2中的一个

enum枚举类型

enum Color {
  red,
  green,
  blue
}

let color = Color.red
console.log(color) // 0

默认情况下,枚举中内容的值是从0开始,依次加1

可以手动指定枚举开始的值:

enum Color {
  red = 7,
  green,
  blue
}

此时Color.red、Color.green、Color.blue分别是7、8、9

除此之外,也可以自定义枚举中的值:

enum Color {
  red = 7,
  green = 10,
  blue = 1
}

也可以将枚举中内容定义为字符串类型,而且枚举中内容的数据类型可以不一样(可以既包含字符串类型又包含数字类型):

enum Color {
  red = 7,
  green = '123',
  blue = '456'
}

any任意类型

当编写代码不知道使用什么类型时,可以使用any来代替

  • any是一个动态类型,支持并兼容所有类型
let value: any = '666'
value = true
value = 1
value = {}
value()
value.toUpperCase()

可以发现,使用了any后,编写出来的内容不会进行强类型校验,可以为any类型的变量赋值为各种类型的内容,还可以将any类型的变量当成函数调用,甚至调取其中不一定存在的方法,有些像JavaScript。

既然使用TypeScript的目的就是为了给JavaScript带来强类型定义,那么为什么还要引入any这种支持所有类型的动态类型呢?

使用any是为了加速开发过程,避免太冗长或没有必要的类型定义,TypeScript中所带来的any极大程度的保留了JavaScript中的灵活性。但是在开发过程中要合理使用any,同时要避免使用any后出现的错误

unknown未知类型

在TypeScript中与any比较类似的类型为unknown未知类型,但是unknown与any不同的是:

unknow虽然与any一样不能保证具体类型是什么,但是unknown可以保证类型安全

比如,将上面代码中的any类型换成unknown

此时,可以发现value()与value.toUpperCase()编译报错,在使用unknown时需要进行一定程度的判断,当确定了变量类型以后才能进行正常的使用:

let value: unknown = '666'
value = true
value = 1
value = {}
if (typeof value === 'function') {
  value()
}
if (typeof value === 'string') {
  value.toUpperCase()
}

所以在项目开发中要结合具体情况来判断使用any还是unknown

  • 使用any虽然可以避免复杂的类型定义来进行项目快速开发快速上线,但是会遗留安全隐患
  • 使用unknown会更加保险一些,可以保证类型安全

void类型

在TypeScript中,一个函数没有任何返回的情况下,这个函数的返回值就是void类型

也可以为函数指定为void类型:

function sayHi(): void{
  console.log('hi')
}

undefined类型

undefined与void不同的是:

  • void表示变量本身就不存在
  • undefined表示变量没有赋值、没有初始化

所以,一但将函数的类型定义为undefined,且没有return时会报如下错误

所以,如果函数为undefined类型,需要写成如下:

function sayHi(): undefined{
  console.log('hi')
  return
}

定义为undefined类型的变量,赋值时必须为undefined

let un: undefined = undefined

null类型

null类型的变量的值也只能是null

let nu: null = null

never类型

当一个函数永远无法执行完成时,其返回值类型可以用never表示

比如,一个抛出异常的函数

当不为该函数指定类型时,由于没有返回任何内容,函数返回值类型会推导为void类型,但是实际上该函数是永远无法执行完成的,因为一执行便抛出了异常因此中断了执行。此时,我们就可以为该函数返回值定义为never类型:

function throwError(message: string, errorCode: number): never {
  throw {
    message,
    errorCode
  }
}

除了抛出异常的函数外,无限循环的函数也是永远无法执行完成的,同样其返回值也可以定义为never类型

function loop(): never {
  while(true) {
    console.log('a')
  }
}

object对象类型

当我们在ts中定义了一个对象后,ts便为该对象定义好了类型:

鼠标移入到person上可以发现,该对象已经被定义好了类型,此类型是一个花括号包裹起来的键值对(键为该对象的属性、值表示该属性对应的数据类型)

在js中,可以调取对象中不存在的属性,而在ts中可以发现会报错。

对象的类型在ts中可以自动推导出来,同时我们也可以手动去指定:

const person: {
  name: string,
  age: number,
  sex: string
} = {
  name: 'zhangsan',
  age: 18,
  sex: 'male'
}

此外,也可以直接将对象定义为object类型

  • 需要注意的是,此方式是一个笼统的定义方式,不但访问不存在的属性会报错,访问对象本身存在的属性也会报错

如果为对象定义为object,相当于为将对象定义为{}(空对象):

这样ts无法判断出里面包含的内容,所以会编译失败。

那么object类型与any类型有什么区别呢?

  • 可以认为object类型更加精确,将一个变量定义为object类型,表示该变量为对象
  • object为any的子集,如果将对象定义为any类型,那么访问不存在的属性也不会报错了

交叉类型

有时候我们想定义一个类型C,比如它即包含了A类型中的内容又包含了B类型中的内容,此时该类型可以使用交叉类型来表示:C= A & B

interface Person {
  name: string
  age: number
}
interface ISchool {
  schoolName: string
  address: string
}

type Student = Person & ISchool

const student: Student = {
  schoolName: '实验小学', 
  address: '101',
  name: 'xiaoming',
  age: 8
}

联合类型

联合类型表示一个值的类型可以是几种类型之一

  • 不同类型之间用"|"分隔
  • 与交叉类型不同的是,交叉类型表示这个类型是几种类型之"和";而联合类型则表示是几种类型之一
function padLeft(val: string, padding: string | number) {
  if (typeof padding === 'number') {
    return Array(padding + 1).join(' ') + val
  }
  if (typeof padding === 'string') {
    return padding + val
  }
  throw new Error('Expected string or number')
}

console.log(padLeft('hello', '1')) // 1hello

如果一个类型是联合类型,那么只能访问这个联合类型中所有类型的共有成员

interface Bird {
  fly()
  layEggs()
}

interface Fish {
  swim()
  layEggs()
}

function getSmallPet(): Fish | Bird {
  //...
}

let pet = getSmallPet()
pet.layEggs()
// pet.swim() // 无法访问到

类型断言

TypeScript中的类型断言主要用于各种类型的适配工作

  • 这里需要注意的是,类型断言是进行类型适配的而不是类型转换

什么是类型适配呢?

比如,我们首先定义了一个any类型的变量,然后将此字符串赋值给该变量。理论上此时该变量为string类型,但是在对此变量使用字符串相关的原型方法时可以发现并没有提示出来,而且将鼠标移到该变量身上可以发现依旧是any类型。

此时,我们需要通知TypeScript编译器该变量为string类型,而这个通知TypeScript类型适配的过程就是“类型断言”。

方式一:通过'<>'尖括号来进行类型断言

let message: any;
message = 'a';
const result = (<string>message).endsWith('a')

方式二:通过as关键字

let message: any;
message = 'a';
const result = (message as string).endsWith('a')

如果一个变量的类型是联合类型,其中联合类型之一是undefined或null

  • 对于这种联合类型是无法直接调取里面的成员方法的(因为TypeScript无法确保该变量不是null 或者 undefined)
  • 对于这种变量来说,如果我们确定传入的值一定是非null或非undefined,可以通过在变量名后面加感叹号的方式来进行断言
function getUpperCase(val: null | string) {
  return val!.toUpperCase()
}
getUpperCase('abc')

类型保护(类型守卫)

场景一:当一个变量的类型是联合类型的时候,我们只能直接访问各类型的共有成员,不能够直接访问某一个类型中特有的成员

场景二:当函数中接收变量是某些类的基类时,我们无法直接访问到这些类的特有成员,只能访问该基类中的成员

此时我们可以通过类型保护来确保该变量是否属于某个类型,实现类型保护的方式有以下几种

  • typeof
  • in关键字
  • instanceof
  • 类型谓词

typeof

function padLeft(val: string, padding: string | number) {
  if (typeof padding === 'number') {
    val.padStart(padding)
  }
  if (typeof padding === 'string') {
    return padding + val
  }
  throw new Error('Expected string or number')
}

console.log(padLeft('hello', '1')) // 1hello

in关键字

interface Bird {
  fly()
  layEggs()
}

interface Fish {
  swim()
  layEggs()
}

function swimOrFly(pet: Bird | Fish) {
  if ('fly' in pet) {
    pet.fly()
  }
  if ('swim' in pet) {
    pet.swim()
  }
}

let pet: Bird = {
  fly: () => {
    console.log('i can fly...')
  },
  layEggs: () => {
    console.log('two eggs')
  }
}

swimOrFly(pet) // i can fly...

instanceof

class Animal {
  move(distance: number = 0) {
    console.log(`move....${distance}`)
  }
}

class Dog extends Animal {
  bark() {
    console.log('Woof!!')
  }
}

class Bird extends Animal {
  layEggs() {
    console.log('two eggs...')
  }
}

function doWhat(pet: Animal) {
  if (pet instanceof Dog) {
    pet.bark()
  }
  if (pet instanceof Bird) {
    pet.layEggs()
  }
}

doWhat(new Dog()) // Woof!!
doWhat(new Bird()) // two eggs...

类型谓词

除了以上的方式外,我们也可以自定义一个返回boolean类型函数

  • 函数的返回值类型定义为:变量名 is 返回值类型 这样的谓词来告知TypeScript返回值的具体类型
function isNumber(padding: any): padding is number {
  return typeof padding === 'number'
}

function isString(padding: any): padding is string {
  return typeof padding === 'string'
}

function padLeft(val: string, padding: string | number) {
  if (isNumber(padding)) {
    return Array(padding + 1).join(' ') + val
  }
  if (isString(padding)) {
    return padding + val
  }
}

console.log(padLeft('hello', 3)) //   hello
console.log(padLeft('hello', '3')) //3hello

六、interface接口

TypeScript中的接口可以描述各种各样的“外形”

  • 比如 可以描述带有属性的普通对象的外形(也就是说,描述这个对象都包含哪些属性、各属性值的类型是什么)
  • 再比如 可以描述一个函数的类型
  • 除此之外 也可以使用接口去描述可索引的类型

接口是如何工作的

比如,在JavaScript中,如果我们在一个函数中期望接收一个包含x, y属性的对象并且属性值是数字类型。我们是无法通过简单的方式判断传入的对象是否符合规范:

可以发现,以上代码在js编译阶段不会报错。

针对以上场景,在TS中可以通过interface接口来约束传入的对象中包含的内容,从而在编译阶段避免不必要的错误。

interface Point {
  x: number
  y: number
}

function drawPoint(point: Point) {
  console.log(point.x, point.y);
}

接口中的可选属性与只读属性

可选属性

当我们用接口来约束传递对象所包含的内容时,如果有些属性不是必须传递的,那么可以采用可选属性来表示

  • 可选属性表示的方式为:属性名?: 值类型

比如,我们根据正方形的配置信息,来创建出一个正方形,其中正方形的颜色信息与宽度是非必传的:

interface Square {
  color: string
  area: number
}
interface SquareConfig {
  // color为非必传字段
  color?: string
  width?: number
}
function createSquare(config: SquareConfig): Square {
  const newSquare = { color: 'white', area: 100 };
  if (config.color) {
    newSquare.color = config.color
  }
  if (config.width) {
    newSquare.area = config.width * config.width
  }
  return newSquare;
}

const mySquare = createSquare({color: 'red'})
console.log('mySqaure..', mySquare) // mySqaure.. { color: 'red', area: 100 }

只读属性

如果对象中某些属性只想在创建对象时进行赋值而不想后续被更改的话,可以使用只读属性来表示

  • 在接口中,可以通过:readonly 属性名: 值类型 来表示只读属性
interface Point {
  readonly x: number
  readonly y: number
}

const p: Point = {x: 1, y: 2}

可以发现,在对对象中的只读属性做修改时会报错:

除了只读属性外,TypeScript还为我们提供了泛型的“只读数组”

  • 我们可以通过 let r: ReadonlyArray = 某数组 来将该数组变为“只读数组”
  • 对于只读数组来说,任何可以改变数组内容的方法都不能使用
  • 也不能修改数组的长度
let array = [1, 2, 3]
let ar: ReadonlyArray<number> = array

使用接口描述函数类型

interface除了可以描述普通对象的“外形”外,还可以用于表示函数类型

  • 在定义函数类型时,只需要在接口中写入函数的参数列表以及返回值类型即可
interface SearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: SearchFunc = function (s: string, sb: string) : boolean {
  let result = s.search(sb)
  return result > -1
}

实现的函数中的参数名可以与接口中定义的函数类型中的参数名不同,只需要保证实现的函数中的参数数目与类型和接口中定义的一致即可。

我们也可以省略实现的函数中的参数类型与函数的返回值类型,此时TypeScript会自动进行类型推断,但此时需要注意的是,函数实际的返回值类型必须要与接口中定义的保持一致:

使用接口描述可索引类型

在接口中可以通过 [索引签名:签名类型]: 索引返回值类型 来描述可索引类型

  • 比如 [index: number]: string 可以表示索引为数字,索引对应内容为字符串的可索引类型
  • TypeScript支持numberstring两种类型的索引签名

比如,我们定义一个索引为数字,索引对应内容为字符串的索引类型

interface StringArray {
  [index: number]: string
}

const myArray: StringArray = ['a', 'b']
console.log(myArray[0]) //a

TypeScript中除了可以将索引定义为number外,也可以定义为string。

比如我们在用接口描述一个对象的时候,无法确定该对象属性名时,可以这样表示:

interface Person {
  name: string
  age: number
  [prop: string]: any
}
const person1: Person = {name: 'zhangsan', age: 18, aaa: 1}
const person2: Person = {name: 'zhangsan', age: 18, bbb: 2}

该接口描述的对象表示除了包含name、age确定的属性外,还包含了其他不确定的属性。

再比如,我们可以使用可索引类型描述出一个数据字典:

interface Person {
  age: number
  sex: string
}

interface PersonDictionary {
  [username: string]: Person
}

const personList: PersonDictionary = {
  zhangsan: {
    age: 18,
    sex: '男'
  },
  lisi: {
    age: 18,
    sex: '男'
  }
}

console.log(personList['zhangsan']) // { age: 18, sex: '男' }

需要注意的时!

在接口中是可以同时使用数字签名与字符串签名的,当同时使用这两种签名的时候,数字索引返回值必须是字符串索引返回值的子类型

  • 这是因为当同时使用这两种索引的时候会将数字类型的索引转换为字符串类型

可以发现当把继承自Animal的Dog赋值为string类型的索引时会报错,正确写法如下:

class Animal {
  name: string
}

class Dog extends Animal {
  breed: string
}

interface NotOk {
  [x: number]: Dog
  [x: string]: Animal
}

索引类型也可以设置为只读的

使用接口描述类类型

与其他语言接口作用一样,TypeScript中也可以用接口去明确一个类去符合某种契约

  • 类实现某个接口要采用关键字 "implements"
  • 一旦类implements了一个接口,就要实现接口中定义的属性与方法

比如,定义一个钟表类接口,其中描述了当前时间属性以及setTime的方法,然后再定义一个钟表类去实现该接口:

interface ClockInterface {
  currentTime: Date
  setTime(d: Date)
}

class Clock implements ClockInterface {
  currentTime: Date
  setTime(d: Date) {
    this.currentTime = d
  }
}

需要注意的是,接口中定义的内容只会对类中的公有成员做类型检查不会对私用成员以及静态部分做类型检查。比如此时在Clock类中加入构造函数,由于构造函数属于类的静态部分,不会做类型检查所以不会报错:

再比如,在Clock中加入私有成员也不会报错:

与类相关的有实例接口构造器接口

  • 上述代码中的ClockInterface表示的是类的实例接口
  • 我们还可以为类定义构造器接口

比如:

interface ClockConstructor {
  new(hour: number, minute: number)
}

但是,需要注意的是,不能用类去实现构造器接口,会报错:

那么我们什么时候使用实例接口,什么时候使用构造器接口呢?

比如:

  • 我们现在定义一个实例接口ClockInterface其中包含tick方法,以及一个构造器接口ClockConstructor,构造器方法返回值类型为实例接口类型:ClockInterface;
  • 然后定义一个工厂函数,其接收的参数为构造器、hour、minute,然后返回一个ClockInterface类型的实例
  • 然后分别定义数字时钟类DigitalClock、指针时钟类AnalogClock去实现ClockInterface实例接口,并实现其中的tick方法
  • 最后使用工厂函数来创建实例
interface ClockInterface {
  tick()
}
interface ClockConstructor {
  new(hour: number, minute: number): ClockInterface
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
  return new ctor(hour, minute)
}

class DigitalClock implements ClockInterface {
  constructor(h: number, m: number) {

  }
  tick() {
    console.log('beep beep')
  }
}

class AnalogClock implements ClockInterface {
  constructor(h: number, m: number) {

  }
  tick() {
    console.log('tick toc')
  }
}

let digitalClock = createClock(DigitalClock, 12, 21)
let analogClock = createClock(AnalogClock, 10, 10)

以上代码中,分别向createClock工厂函数中传入了DigitalClock、AnalogClock,这两种类的构造函数均满足构造器接口中定义的构造函数签名

接口之间的继承

与类一样,接口之间也可以继承

  • 这是一个非常有用的特性,可以实现将一个接口中的内容赋值到另一个接口里,可以方便我们将接口分隔到可重用的模块中
interface Shape {
  color: string
}

interface Square extends Shape {
  sideLength: number
}

const s = {} as Square
s.color = 'red'
s.sideLength = 10

接口也可以继承多个接口,多个接口之间用逗号分隔

interface Shape {
  color: string
}

interface PenStroke {
  penWidth: number
}
interface Square extends Shape, PenStroke {
  sideLength: number
}

let s = {} as Square
s.color = 'red'
s.sideLength = 10
s.penWidth = 10

使用接口定义混合类型

有时我们期望一个对象中即包含某些属性,又包含一些方法等,那么此时也可以用接口去描述

interface Counter {
  (start: number): string
  interval: number
  reset(): void
}

function getCounter(): Counter {
  let counter = (function (start: number) {
    console.log('start..', start)
  } )as Counter
  counter.interval = 100
  counter.reset = function() {}
  return counter
}

let c1 = getCounter()
console.log(c1.interval) // 100
c1(10) // start...10

七、class类

类的基本示例

class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message
  }
  greet() {
    return 'hello, '+ this.greeting
  }
}

let greeter = new Greeter('world')
console.log(greeter.greet())// hello, world

比如声明了一个Greeter类,其中包含了三个成员变量:greeting属性、构造函数、greet方法

  • 使用new 关键字去构造了一个Greeter类的实例greeter,构造时会调取构造函数执行里面的代码

类的继承

可以通过继承来扩展类

  • 继承使用关键字 extends
class Animal {
  move(distance: number = 0) {
    console.log(`move....${distance}`)
  }
}

class Dog extends Animal {
  bark() {
    console.log('Woof!!')
  }
}

const dog = new Dog()
dog.move(100)
dog.bark()

比如,有一个父类Animal其包含了一个move方法,然后定义一个Dog类继承自Animal(Dog类可看作Animal的子类),此时实例化Dog类后,dog对象便可调用其本身的bark方法,也可以使用父类中的move方法。

  • 当父类中定义了构造方法,子类在继承父类后其构造方法中第一行必须要通过super关键字调取父类的构造方法,如果还想为本类中的属性赋值,那么this.xx=xx要写在后面
  • 在子类中,可以对父类中的方法进行重写,在重写的方法中如果希望调取父类中的方法,那么可以通过super.xx的方式来调取父类中的方法
class Animal {
  name: string
  constructor(name: string) {
    this.name = name
  }
  move(distance: number = 0) {
    console.log(`${this.name} move....${distance}`)
  }
}
class Cat extends Animal {
  constructor(name: string) {
    super(name)
    // this.xx = xx
  }
  move(distance: number = 10) {
    console.log('miaomiao...')
    super.move(distance)
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name)
  }
  move(distance: number = 45) {
    console.log('wang...')
    super.move(distance)
  }
}

const cat = new Cat('Tom');
const dog = new Dog('Jeck');
cat.move(45)
dog.move(100)

类中的修饰符

修饰符分为:

  • 公有 public
  • 私有 private
  • 受保护的 protected
  • 只读 readonly

public

在TypeScript中如果成员不添加修饰符,默认为public(公有的) ,公有的属性即方法在类的内部和外部都可以随意访问并修改。

class Animal {
  name: string
  constructor(name: string) {
    this.name = name
  }
  move(distance: number = 0) {
    console.log(`${this.name} move....${distance}`)
  }
}
new Animal('aa').name = 'cc' // 可访问,也可修改其中的public属性

相当于:

class Animal {
  public name: string
  public constructor(name: string) {
    this.name = name
  }
  public move(distance: number = 0) {
    console.log(`${this.name} move....${distance}`)
  }
}
new Animal('aa').name = 'cc' // 可访问,也可修改其中的public属性

private

当属性或方法被private修饰后,只能在该类的内容访问,在其子类与外部是访问不到的,比如:

需要注意的是!

如果类实现了一个接口,该接口里不可以定义出类中的私有成员

比如现在有一个Point类实现了IPoint接口,但是其中的x y成员是私有的,但是x y在接口中定义了出来,此时会报错

此时我们需要在接口中去掉私有成员的定义:

interface IPoint {
  drawPoint: () => void
}

class Point implements IPoint {
  private x: number
  private y: number

  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }
  drawPoint = () => {
    console.log('x...', this.x, 'y...', this.y)
  }
}

protected

当属性或方法被protected修饰后,只能在本类与其子类中访问,外部是访问不到的:

class Person {
  protected name: string
  constructor(name: string) {
    this.name = name
  }
}
class Employee extends Person {
  private department: string

  constructor(name: string, department: string) {
    super(name)
    this.department = department
  }
  getElevatorPitch() {
    console.log(`hello, my name is ${this.name} and I work in ${this.department}`)
  }
}

const employee = new Employee('zhangsan', 'Sales')
employee.getElevatorPitch()

可以发现,在子类的getElevatorPitch方法中是可以访问父类中的name属性的(受保护的属性),但是在外部我们是无法访问父类中的name属性的:

同样,如果我们不期望一个类的父类可直接被实例化,那么可以将其父类的构造函数添加上protected修饰符:

readonly

有时候我们期望有些属性在外部是可以被访问到的,但是不可以被修改,此时可以使用readonly修饰符来修饰

对于在类中声明完成员属性后,需要在构造函数中对这些属性进行赋值的情况,可以通过在构造函数参数中添加上修饰符的方式来简化

  • 加上修饰符后,相当于既定义了该成员又在构造函数中进行了赋值操作

比如,在Person类中有一个name属性,在其构造函数中需要对name属性进行赋值

class Person {
  name: string
  constructor(name: string) {
    this.name = name
  }
}
new Person('aa')

对于这种情况,我们可以在其构造函数中的name参数前加上修饰符来简化,就不需要在类中再定义一遍成员属性了:

class Person {
  constructor(public name: string) {}
}
const p = new Person('aa')
console.log(p.name) // aa

除了可以使用public外,也可以使用private、protected、readonly

类中的存取器

TypeScript中支持使用存取器来访问内部成员

  • 场景一:当需要对外提供私用成员的访问时
  • 场景二:当我们想在访问或是修改成员属性时触发额外的逻辑,可以采用存取器来实现

通过存取器来访问内部成员相当于建立了一个”缓冲带“,在访问成员的同时可以用来避免非法操作带来的影响

场景一:比如Point类中有两个私有成员属性x与y,当想对外提供访问私有属性时,可以通过getX getY方法来实现,想对外提供修改x,y途径时可通过setX setY来实现

interface IPoint {
  drawPoint: () => void
  getDistance(p: IPoint): number
  getX(): number
  getY(): number
}

class Point implements IPoint {
  private x: number
  private y: number

  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }
  drawPoint = () => {
    console.log('x...', this.x, 'y...', this.y)
  }
  getDistance(p: IPoint): number {
    return Math.pow(p.getX() - this.x, 2) + Math.pow(p.getY()-this.y, 2)
  }

  getX() {
    return this.x
  }

  getY() {
    return this.y
  }

  setX(val: number) {
    if (val < 0) {
      throw new Error('val不能小于0...')
    }
    this.x = val
  }
  setY(val: number) {
    if (val < 0) {
      throw new Error('val不能小于0...')
    }
    this.y = val
  }
}

const point1 = new Point(1, 2)
const point2 = new Point(2, 3)
console.log(point1.getX()) // 1
console.log(point1.getY()) // 2
console.log(point1.getDistance(point2)) // 2
point1.setX(-10) // Error: val不能小于0...

除了可以通过getX、setX这种驼峰形式的方式来命名get、set函数名外,还可以通过set xxx()、get xxx()的方式来指定对外暴露的私有成员访问时的属性名

  • 这样就不用在访问私用成员时调用set与get方法了,可以通过对象.属性名的方式来访问
  • 此时为了区分访问私用成员时的属性名与内部私用成员的属性名,可以为类内部的私用成员属性名前加上下划线"_"(具体以公司内部规范为准)

比如Employee中有私有属性fullName,我们使用存取器进行访问该私有属性,当使用其中的get方法修改fullName时,需要额外判断一下密码是否正确:

let password = '123456'

class Employee {
  private _fullName: string
  get fullName(): string {
    return this._fullName
  }
  set fullName(newName: string) {
    if (password && password === '1234567') {
      this._fullName = newName
    } else {
      console.log('Error...')
    }
  }
}

const employee = new Employee()
employee.fullName = 'john' // Error...

需要注意的是,在采用tsc编译该ts文件时,会报以下错误:

因为TypeScript默认是编译成ES3的,而存取器是ES5才支持的语法,我们需要使用tsc xx.ts --target es5来指定TypeScript编译为ES5语法

此时可以发现运行成功

当打开编译后的代码后,可以发现,存取器本质是通过Object.defineProperty来实现的,当我们访问fullName时会触发其get方法,修改 fullName时会触发其set方法:

var password = '123456';
var Employee = /** @class */ (function () {
  function Employee() {
  }
  Object.defineProperty(Employee.prototype, "fullName", {
    get: function () {
      return this._fullName;
    },
    set: function (newName) {
      if (password && password === '1234567') {
        this._fullName = newName;
      }
      else {
        console.log('Error...');
      }
    },
    enumerable: false,
    configurable: true
  });
  return Employee;
}());
var employee = new Employee();
employee.fullName = 'john';

如果类实现了接口,且类中定义了私用成员与访问私有成员的方法,形如set xx(...)get xx() ,那么在该接口中就要采用如下方式取定义

interface IPoint {
  drawPoint: () => void
  getDistance(p: IPoint): number
  X: number
  Y: number
}

class Point implements IPoint {
  private x: number
  private y: number

  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }
  drawPoint = () => {
    console.log('x...', this.x, 'y...', this.y)
  }
  getDistance(p: IPoint): number {
    return Math.pow(p.X - this.x, 2) + Math.pow(p.Y-this.y, 2)
  }

  get X() {
    return this.x
  }

  get Y() {
    return this.y
  }

  set X(val: number) {
    if (val < 0) {
      throw new Error('val不能小于0...')
    }
    this.x = val
  }
  set Y(val: number) {
    if (val < 0) {
      throw new Error('val不能小于0...')
    }
    this.y = val
  }
}

const point1 = new Point(1, 2)
const point2 = new Point(2, 3)
point1.X = 10
console.log(point1.X) // 10
console.log(point1.Y) // 2
console.log(point1.getDistance(point2)) // 65

类中的静态属性

静态属性存在于类本身上,使用static修饰,可以通过类本身直接访问

class Grid {
  static origin = {x: 0, y: 0}
  
  scale: number
  constructor(scale: number) {
    this.scale = scale
  }
  calculate(point: {x: number, y: number}) {
    let xDist = point.x - Grid.origin.x
    let yDist = point.y - Grid.origin.y
    console.log(Math.sqrt(xDist * xDist + yDist * yDist) * this.scale)
  }
}

const grid1 = new Grid(1)
const grid2 = new Grid(2)
grid1.calculate({x: 3, y:4}) // 5
grid2.calculate({x: 3, y:4}) // 10

抽象类

抽象类一般作为其他派生类的基类使用,不能直接被实例化

  • 抽象类中可以包含抽象方法,该抽象方法是不能直接实现的,必须在其派生类中实现且必须要实现,抽象方法是不包含方法体的
  • 抽象类中也可以包含成员属性与方法
  • 我们可以巧妙的采用抽象类去提取出抽象的方法,这样可以使得继承自它的派生类能够按照一定的约束去实现,提高了代码的复用性于可维护性
  • 抽象类与抽象方法前需要添加 abstract 关键字
  • 抽象方法的abstract关键字前也可以添加public 、private 、protected修饰符来修饰
abstract class Department {
  name: string
  constructor(name: string) {
    this.name = name
  }
  printName(): void {
    console.log('DepartmentName...' + this.name)
  }
  abstract printMeeting(): void
}

class AccountingDepartment extends Department {
  constructor() {
    super('Accounting')
  }
  // 必须要实现其基类中的抽象方法
  printMeeting(): void {
    console.log('AccountingDepartment meets')
  }
  // 该类中自己的成员方法
  reports() {
    console.log('reports...')
  }
}

let department: AccountingDepartment
department = new AccountingDepartment()
department.printName() // DepartmentName...Accounting
department.printMeeting() // AccountingDepartment meets
department.reports() // reports...

八、函数

函数的基本示例

与JavaScript一样,在TypeScript中也是可以创建命名函数与匿名函数的

// 命名函数
function add(x: number, y: number) {
  return x + y
}

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

函数类型

TypeScript中,可以为函数的参数与返回值添加类型

function add(x: number, y: number): number {
  return x + y
}

TypeScript也可以根据返回值自动推断出函数的返回值类型,一旦函数的返回值的类型与定义的返回值类型不符,则会报错:

一个完整的函数类型包括其参数类型以及返回值类型

let myAdd: (baseValue: number, increment: number) => number = function(x: number, y: number) {
  return x+ y
}

函数中的可选参数

在TypeScript中传入函数的参数个数必须要与期望的参数个数保持一致

在TypeScript中可以在参数后面跟上问号来表示该参数是可选的(可传可不传)

function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return firstName + lastName
  }
  return firstName
}

buildName('1')

可选参数必须跟在比传参数的后面

函数中的默认参数

在TypeScript中也可以为参数提供默认值,当不传递该参数或显式传递成undefined时将采用默认值

function buildName(firstName: string, lastName = 'a'): string {
  return firstName + lastName
}

console.log(buildName('1')) // 1a
console.log(buildName('2', undefined)) // 2a

默认参数不用必须写在必传参数的后面,如果默认参数写在了必传参数的前面的话,那么我们此时必须要明确传递undefined才能使其采用默认值

function buildName(lastName = 'a', firstName: string): string {
  return firstName + lastName
}

console.log(buildName(undefined, '1')) // 1a

函数中的剩余参数

同样,在TypeScript中,当函数中接收的参数无法确定时,也可以使用剩余参数

function buildName(firstName: string, ...restOfNames: string[]): string {
  return firstName + restOfNames.join('')
}
console.log(buildName('1')) // 1
console.log(buildName('1', '2', '3')) // 123

函数的重载

相对于JavaScript来说,TypeScript中加入了函数的重载

  • 函数重载指的是:函数名相同、但是函数参数列表与函数返回值不同

比如,我们如果想实现一个相加 / 拼接的函数,该函数中可以传递数字与字符串来进行相加 /拼接:

function add(num1: string | number, num2: string | number) {
  if(typeof num1 === 'number' && typeof num2 === 'number') {
    return num1 + num2
  }
  if (typeof num1 === 'string' && num2 === 'string') {
    return num1 + num2
  }
}

可见如果采用联合类型来实现的话,我们需要大量的逻辑判断

针对这种情况我们考虑采用函数重载的方式来实现:

  • 首先要定义出来函数重载的函数类型
  • 最后再写函数的具体实现
// 定义函数
function add(num1: number, num2: number): number;
function add(num1: string, num2: string): string;

// 实现函数
function add(num1: any, num2: any): any {
  return num1 + num2
}

add(1, 2)
add('1', '2')

九、泛型

在TypeScript中使用泛型可以提高接口、类、函数等的可重用性

  • 泛型是指在定义接口、类、函数时不事先是定好类型,而是根据实际需要来传入所需要的类型

在函数中使用泛型

比如,在一个函数中我们想根据传入参数的类型来决定返回值类型(传入数字返回数字类型,传入字符串则返回字符串类型)

一种方式是我们可以将传入参数的类型与返回值类型都定义为any,但是这样的话会导致类型丢失,而且并不能保证返回值类型就是我们想要的:

比如以上实例中,传入数字2,如果我们想要获取的返回值也是数字类型的,这样是不能够保证肯定是数字类型的。

这种场景我们就可以考虑使用泛型:

  • 我们可以使用 的方式来表示后续要接收的类型
  • 然后在使用函数的时候在函数名后面跟上<具体类型>来明确类型
function identity<T>(arg: T): T {
  return arg
}
identity<number>(2)
identity<string>('2')
identity(1) // 此时会根据传入的参数类型自动推断出所需要的类型

泛型接口

在定义接口的时候,也可以使接口去接收泛型

function identity<T>(arg: T): T {
  return arg
}
interface GenericI<T> {
  (arg: T): T
}

let myIdentity: GenericI<number> = identity

泛型类

同泛型接口一样,类名后也可以跟上来接收泛型

  • 类中包括静态部分和实例部分,需要注意的是泛型指的是实例部分的泛型,类的静态部分是不能使用泛型的
class GenericClass<T> {
  zeroVal: T
  add: (x: T, y: T) => T
}

let myGeneric = new GenericClass<number>()
myGeneric.zeroVal = 0
myGeneric.add = function(x, y) {
  return x + y
}

let stringG = new GenericClass<string>()
stringG.zeroVal = 'hello'
stringG.add = function(x, y) {
  return x + y
}

console.log(stringG.add(stringG.zeroVal, ',world')) // hello,world

泛型约束

我们可以通过泛型继承接口的方式来约束泛型

比如,在一个使用了泛型的方法中需要获取泛型的length属性的,但是此时会报错,因为TypeScript无法确定该泛型上是否包含了length属性:

此时可以使用接口来为泛型增加约束:

interface Lengthwise {
  length: number
}

function test<T extends Lengthwise>(arg: T): T {
  console.log('arglength...', arg.length)
  return arg
}
test('111')
// test(1) 编译不会通过,因为数字类型上没有length属性

再比如,在一个方法中,我们期望访问到对象属性对应的值,要保证传入的属性名在对象上必须是存在的,此时我们也可以通过泛型约束来实现

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]
}
const o = {a: 1, b: 2, c: 3}
getProperty(o, 'a')
// getProperty(o, 'e') 报错,因为e属性不在o上

getProperty接收两个泛型,一个代表obj的类型,一个代表key的类型

  • 其中为将key的泛型通过extends keyof T的方式来约束在了obj属性范围内
  • 这样一旦传入了obj上不存在的属性便会报错

泛型约束也可以用在工厂函数中

  • 比如根据传入的构造器去实例化不同类的对象,此时TypeScript会根据泛型约束推导出该对象属于哪个类的实例
class LionKeeper {
  nametag: string
}

class BeeKeeper {
  hasMask: boolean
}

class Animal {
  numLengs: number
}

class Bee extends Animal {
  keeper: BeeKeeper
}

class Lion extends Animal {
  keeper: LionKeeper
}

function createInstance<T extends Animal>(c: new() => T): T {
  return new c()
}
createInstance(Lion).keeper.nametag
createInstance(Bee).keeper.hasMask

十、typeof操作符

typeof除了可以用作类型保护外,还可以用于从已有数据类型的变量中提取其类型

const center = {
  x: 0,
  y: 0,
  z: 0
}

type Point = {
  x: number
  y: number
  z: number
}

const unit: Point = {
  x: center.x + 1,
  y: center.y + 1,
  z: center.z + 1
}

针对以上的情况,center数据本身包含了x, y, z数据,然后我们又定义了一个Point类型包含x, y, z这样是没有必要的,此时我们可以直接通过 typeof center来获取其类型供unit使用:

const center = {
  x: 0,
  y: 0,
  z: 0
}

type Point = typeof center

const unit: Point = {
  x: center.x + 1,
  y: center.y + 1,
  z: center.z + 1
}

// 或者写为
const unit2: typeof center = {
  x: center.x + 1,
  y: center.y + 1,
  z: center.z + 1
}

十一、keyof操作符

通过keyof关键字可以获取到一个对象中所包含的键值,可用于在获取一个对象中的属性值时限制传入的key在该对象包含key的范围内,从而避免不必要的错误

比如,以下代码中,我们访问一个对象中不存在的属性时,编译不会报错:

此时,我们可以通过联合类型限制key的取值范围,但这种方式需要手动去编写,比较麻烦而且容易写错,而且如果后续修改了类型Person,还需要手动去修改联合类型:

针对这种场景我们就可以通过keyof来动态获取Person类型中所包含的key

type Person = {
  name: string
  age: number
  location: string
}

type PersonKey = keyof Person

const tom: Person = {
  name: 'tom',
  age: 33,
  location: '北京'
}

function getValueByKey(obj: Person, key: PersonKey) {
  return obj[key]
}

// console.log(getValueByKey(tom, 'email')) // 报错
console.log(getValueByKey(tom, 'age'))

此外,我们可以通过泛型的方式来使方法变得更加通用

  • 比如对象的泛型指定为O,对象key的类型指定为K
  • 此时可通过 K extends of keyof O 的方式来限定传入的key在对象key的范围内
type Person = {
  name: string
  age: number
  location: string
}

type PersonKey = keyof Person

const tom: Person = {
  name: 'tom',
  age: 33,
  location: '北京'
}

function getValueByKey<O, K extends keyof Obj>(obj: O, key: K) {
  return obj[key]
}

console.log(getValueByKey(tom, 'age'))

十二、类型查找

当通过TS定义一个后台返回的复杂数据类型后,如果在代码逻辑处理中使用到了这个复杂类型中的其中一部分类型,我们有两种方式进行处理:

  • 将这部分类型抽取出来
  • 使用类型查找的方式来找到这部分数据类型

比如,现定义了如下的复杂类型:

export type ResponseData = {
  user: {
    name: string
    email: string
    gender: string
    address: {
      city: string
      province: string
      contry: string
    }[]
  }
  payment: {
    creditCardNumber: string
  }
}

假设现在有一个函数用来输入该用户的payment信息,该函数的返回值类型即ResponseData类型中定义的payment类型,为了方便函数返回值使用该类型信息,可以将payment类型提取出来:

export type ResponseData = {
  user: {
    name: string
    email: string
    gender: string
    address: {
      city: string
      province: string
      contry: string
    }[]
  }
  payment: Payment
}
type Payment = {
  creditCardNumber: string;
}
function getPayment(): Payment {
  return {
    creditCardNumber: '123123123'
  }
}

另一种方式就是通过类型查找的方式来实现,从而避免片段化的类型声明

  • 类型查找即通过 类型[类型索引名称] (类似于访问对象中的属性值)这种方式来获取想要的部分类型
function getPayment(): ResponseData['payment'] {
  return {
    creditCardNumber: '123123123';
  }
}

通过这种方式也可以方便获取到嵌套类型

  • 如果这个类型是一个数组形式,也可通过数组索引的方式来得到数组中存放的数据类型
function getAddress(): ResponseData['user']['address'][0] {
  return {
    city: '111',
    province: '222',
    contry: '333'
  }
}

十三、类型映射

使用类型映射可以批量定义出一个类型中所需要的属性

  • 需要注意的是,要使用type关键字来定义
  • 当我们需要基于原有类型进行进一步改造或想批量定义属性时,可采用这种方式

比如,我们想定义一个包含x、y、z属性的类型:

type Point = {
  [key in 'x' | 'y' | 'z']: number
}

const p: Point = {
  x: 0,
  y: 0,
  z: 0
}

我们也可以基于原有类型去定义,比如在原有类型Point中的x、y、z是可以修改的,现去将x、y、z定义为不可修改的:

interface Point {
  x: number
  y: number
  z: number
}

type ReadonlyPoint = {
  readonly [key in keyof Point]: number
}

const p: ReadonlyPoint = {
  x: 10,
  y: 10,
  z: 10
}

以上代码中,属性值类型是统一的,那么如果原有类型中属性值对应的类型更新了怎么办?

  • 此时我们可以结合类型查找来实现动态映射属性值类型
type ReadonlyPoint = {
  readonly [key in keyof Point]: Point[key]
}

同样,我们可以结合泛型,来使得这个“只读化”操作变得更通用

  • 需要注意的是如果将该类型定义为Readonly的话,此时要使用export 导出不然会报错,因为TS内部已经为我们实现好了Readonly
// 由于TS内部实现好了Readonly<T>,所以我们要使用export导出我们自定义的Readonly<T>
export type Readonly<T> = {
  readonly [key in keyof T]: T[key]
}

// 如果名字不是Readonly,那不export也不会报错
type ReadonlySome<T> = {
  readonly [key in keyof T]: T[key]
}

我们也可以在类型映射的同时添加上修饰符

比如,在类型映射的过程中加上可选操作符:

type Mapped<T> = { 
  readonly [P in keyof T]?: T[P]
}

在类型映射的同时,我们还可以通过在修饰符前面加上"-"减号来实现去掉修饰符的操作

比如在原本的Point类型中x是只读的,y是可选的,但是在映射该类型时我们期望x去掉只读修饰符,y变成不可选:

interface Point {
  readonly x: number
  y?: number
}

type MappedPoint = {
  -readonly [key in keyof Point] -? : Point[key]
}

接下来,我们看一个类型映射在实际场景中的使用:

比如,我们现在有一个类,在类中提供了update方法来对传入的数据进行更新

export class State<T> {
  constructor(public current: T){}
  update(next: T) {
    this.current = {...this.current, ...next}
  }
}

const state = new State({x: 0 , y: 0})
state.update({x: 0, y: 123})
console.log(state.current) // { x: 0, y: 123 }

通过以上代码来看,在update中需要传入的数据类型于实例化时传入的数据类型要保持一致。但实际上只是想更新y的值,对于这种数据少的情况影响不大,但是对于庞大的数据来说这种方式就会很痛苦。那么有没有一种方式可以传入片段化数据呢?

此时就可以结合类型映射来实现:可以基于T类型来映射一个属性是可选的类型,这样在调取update方法时我们只需要传入{ y: 123 }就可以啦。

export class State<T> {
  constructor(public current: T){}
  update(next: Partial<T>) {
    this.current = {...this.current, ...next}
  }
}

type Partial<T> = {
  [P in keyof T]?: T[P]
}
const state = new State({x: 0 , y: 0})
state.update({y: 123})
console.log(state.current) // { x: 0, y: 123 }