阅读 1168

TypeScript从入门到项目实战(进阶篇)

开篇介绍(实在不知道叫啥)

嗨嗨嗨,大家好,我是八字昵称,我又回来了。

上上周不是说要统一技术栈吗,首个要统一的就是TypeScript,于是就有了这一系列文章,今天写完了进阶篇,既然写出来了,就要发出来帮(水)助(波)不会(经)的同学(验)。

声明(还是这个声明):✨本系列文章为基础教程,不涉及过深的东西,比较适合新手看,如果已经会的同学,看完这段文字就可以跑路了😋。

内置对象的使用

JavaScript中有许多内置对象,可以直接在JavaScript程序中使用,同样的,TypeScript也延续了这些内置对象

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在全局作用域里的对象

ECMAScript内置对象

ECMAScript标准提供的内置对象有:
ObjectErrorNumberDateBooleanArray等:

const boolean: Boolean = new Boolean(true)
const date: Date = new Date()
const n: Number = new Number(20)复制代码

更多内置对象,请看JavaScript 标准内置对象

DOM、BOM内置对象

DOM和BOM常用的有:

Document、HTMLElement、Event、NodeList、Node、等,在DOM操作中很有用

let body: HTMLElement = document.body
let allDiv: NodeList = document.querySelectorAll('div')
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
})复制代码

更多请看文档对象模型 (DOM)


函数

函数中的this

在JavaScript中,this使用可能有时候并不是如自己所想的那样

image.png

但是好消息是,TypeScript中会提示你是否正确的使用了this。

如下面的例子中,SVGElement是window的一个方法,所以编辑器会报错:

document.querySelector('body').addEventListener('click',function (event:MouseEvent) {
  this.nodeName
  this.SVGElement//Property 'SVGElement' does not exist on type 'HTMLBodyElement'.
})复制代码

箭头函数与this

首先来个例子image.jpeg

const awardsInfo = {
  name: '陈灵十',
  age: '10',
  prize: '三等奖',
  takePart() {
    return function(){
      console.log(`姓名:${this.name},年龄:${this.age},参加全国青少年科技创新大赛,获得${this.prize}`)
    }
  }
}

const awards=awardsInfo.takePart()
//期待:姓名:陈灵十,年龄:10,参加全国青少年科技创新大赛,获得三等奖
//结果:姓名:undefined,年龄:undefined,参加全国青少年科技创新大赛,获得undefined
awards()复制代码

可以看到,这里预期的结果和实际结果并不一致。原因其实也很简单,因为awards运行在全局作用域下,而调用时,awards中的this为window,而window并没有nameageprize这些属性。

知道原因我们就可以很好的解决这个问题,利用ES6的箭头函数:

takePart() {
    return () => {
      console.log(`姓名:${this.name},年龄:${this.age},参加全国青少年科技创新大赛,获得${this.prize}`)
    }
  }复制代码

箭头函数有几个使用注意点。

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

更多关于ES6箭头函数请看传送门:箭头函数

this参数

现在有一个超人岑一诺,定义了两个接口:超能力(SuperPowers)、超人(SuperHuman),代码如下:

interface SuperPowers {
  name: string,
  describe: string;
  toString: () => string;
}

interface SuperHuman {
  name: string;
  sex: '男' | '女';
  superPowers: Array<SuperPowers>;
  introduce: () => Function
}

const CenYinuo: SuperHuman = {
  name: '岑一诺',
  sex: '女',
  superPowers: [
    {
      name: '日均作词牌三百首',
      describe: '使用自己开发的文学机器人,在十分钟内快速作词,十分钟之后做出的词,李清照见了羞愧难当,' +
        '辛弃疾见了直接弃文,王安石见了直接拜师,苏轼见了直接退出唐宋八大家的群',
      toString() {
        return `${this.name}:${this.describe}`
      }
    },
    {
      name: '日均作诗两千首',
      describe: '使用自己开发的文学机器人,半小时内快速作诗,半小时后,李白见了开头让出诗仙称号,杜甫未闻其诗听后自惭形秽',
      toString() {
        return `${this.name}:${this.describe}`
      }
    }
  ],
  introduce(): Function {
    return () => {
      const power1 = this.superPowers[0]
      const power2 = this.superPowers[1]
      console.log(`姓名:${this.name}\n性别${this.sex}\n超能力:\n\t1.${power1.toString()},\n\t2.${power2.toString()}`)
    }
  }
}
const introduce = CenYinuo.introduce()
introduce()
// 姓名:岑一诺
// 性别女
// 超能力:
//  ……复制代码

虽然代码能够正常运行(这段代码本身没有问题),但是有一个问题:TypeScript无法正确推断出其中数组的类型

image.pngimage.png

这个问题并无大碍,但是开发起来没有那么舒服了:假如superPowers里面有很多复杂的类型,这时候开发起来就会很难受(不得不说TS的编辑器提示是真的好用)。既然发现了问题,该如何解决呢,这时候可能就有人会说了:

那就解决提出问题的人!

老子反手就是一个TM四连:

image.pngimage.jpegimage.jpegimage.jpeg

其实要解决这个问题也很简单,我们只要告诉TypeScript这个函数的this是什么类型就可以了,没错,就是this参数

当你将一个函数传递到某个库函数里稍后会被调用时。 因为当回调被调用的时候,它们会被当成一个普通函数调用, this将为undefined。 稍做改动,你就可以通过 this参数来避免错误。


首先,库函数的作者要指定 this的类型;

然后函数要被调用,这样TypeScript才能检测到this的类型。

我们只需要对接口SuperHuman进行一点点改动image.jpeg

interface SuperHuman {
  name: string;
  sex: '男' | '女';
  superPowers: Array<SuperPowers>;
  introduce: (this:SuperHuman) => Function
}复制代码

我们可以看到这个时候TypeScript已经检测到了数组中解析出来的类型:

image.pngimage.png


函数重载

什么是函数重载?摘一段维基百科的原话:

函数重载(英语:function overloading),是AdaC++C#、D和Java编程语言中具有的一项特性,这项特性允许创建数项名称相同但输入输出类型或个数不同的子程序,它可以简单地称为一个单独功能可以执行多项任务的能力。

在TypeScript中,允许我们为函数定义不同参数返回不同类型,例如下面的例子:

function reverse(val: number): number
function reverse(val: string): string
function reverse(val: Array<any>): Array<any>
function reverse(val: number | string | Array<any>): number | string | Array<any> {
  if (typeof val === 'number') {
    return +val.toString().split('').reverse().join('')
  } else if (typeof val === 'string') {
    return val.toString().split('').reverse().join('')
  } else {
    return val.reverse()
  }
}

console.log(reverse(2020))
console.log(reverse('hello word!'))
console.log(reverse([1, 2, 3, 4]))
// 输出结果:
// 202
// !drow olleh
// [ 4, 3, 2, 1 ]复制代码

是不是感觉很神奇,TypeScript类型检查通过,让我们分别看看上面三个函数的类型检测为什么:


reverse(2020)检测类型为function reverse(val: number): number

image.png

reverse('hello word!')检测类型为function reverse(val: string): string

image.png

reverse([1, 2, 3, 4])检测类型为function reverse(val: Array<any>): Array<any>

image.png

这个例子已经初见其好处,对于一些复杂逻辑的函数,重载能够发挥更大的作用

(英语:class)在面向对象编程中是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性方法

类的更严格的定义是由某种特定的元数据所组成的内聚的包。它描述了一些对象的行为规则,而这些对象就被称为该类的实例。类有接口和结构。接口描述了如何通过方法与类及其实例互操作,而结构描述了一个实例中数据如何划分为多个属性。类是与某个层[注 1]的对象的最具体的类型。类还可以有运行时表示形式(元对象),它为操作与类相关的元数据提供了运行时支持。

虽然JavaScript中有类的概念,但是很多前端同学并不是很熟悉,一来是不是经常使用,二来,es6之前的类,都不像是类,而是像函数,因为es6之前都是用构造函数实现类的:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);复制代码

和传统的面向对象语言(Java、C++、C#等)相比,JavaScript的类的实现就显得很“怪异”,可能他们的内心OS是这样的:

为了让JavaScript更加有排面,ES6之后对传统的类进行类封装,新增Class(类),其本质是传统类的语法糖,本文假设你已经知道了ES6+的Class(如果你还不知道,请进入传送门:Class的基本语法),TypeScript作为JavaScript的扩展,在支持ES6+的Class的同时,还对这种语法进行了扩展,让Class更加灵活、更加的“面向对象”,所以我们不详说ES中的class,我们只说TypeScript对class的扩展。


语法(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法让程序更加简洁,有更高的可读性。


修饰符

TypeScript为Class扩展了三个修饰符:

  • public:修饰的属性或者方法是公有地,可以在任何地方访问到修饰的属性或者方法,这也是属性或者方法的默认值
  • private:表示属性或者方法是私有的,不能够在类之外访问,如果在外部访问会报错(但是能够通过编译)
  • protected:让属性和方法受保护,能够在类内部操作,但是只能在子类中被访问。

这三个修饰符在传统的面向对象语言中非常常见,如果你学过Java、C++之类的语言,会感到很亲切,但是如果你直接出国JavaScript,可能需要一点时间去接受它。

class Animal {
  private className = '动物类'
  protected des = '这是动物类'
  public species: string

  constructor(species: string) {
    this.species = species
  }
}

class Dog extends Animal {
  name: string

  constructor(varieties: string, name: string) {
    super('狗')
    console.log(this.species)
    console.log(this.des)
    this.name = name
  }
}

const dog = new Dog('中华田园犬', 'dog')
const animal=new Animal('马')
console.log(animal.className)//Property 'className' is private and only accessible within class 'Animal'.
console.log(animal.des)//Property 'des' is protected and only accessible within class 'Animal' and its subclasses.
dog.des//Property 'des' is protected and only accessible within class 'Animal' and its subclasses.复制代码

注意:

当使用private修饰构造函数时,该类不允许被初始化或者被继承(这时候编译还是能够通过,只是TypeScript会报错):

class Animal {
  private constructor() {
  }
}

//Cannot extend a class 'Animal'. Class constructor is marked as private.
class Dog extends Animal {
  constructor() {
    super()
  }
}

const dog = new Dog()
const animal = new Animal()//Constructor of class 'Animal' is private and only accessible within the class declaration.复制代码

当使用protected修饰构造函数时,只能够被继承:

class Animal {
  protected constructor() {
  }
}

class Dog extends Animal {
  constructor() {
    super()
  }
}

const dog = new Dog()
const animal = new Animal()//Constructor of class 'Animal' is private and only accessible within the class declaration.复制代码


参数属性

静态属性(public)

使用public修饰参数时,表示定义属性并为其赋值,能够使代码更加简洁:

class Calendar extends Date {
  constructor(public year: number, public month: number) {
    super()
  }
}

const c: Calendar = new Calendar(2020, 7)
console.log(c.year)
console.log(c.month)复制代码

上面代码等同于下面的代码:

class Calendar extends Date {
  year:number
  month:number
  constructor(year:number,month:number) {
    super()
    this.year=year
    this.month=month
  }
}

const c: Calendar = new Calendar(2020,7)
console.log(c.year)
console.log(c.month)复制代码


只读属性(readonly)

当你想某个值除了初始化,其他任何时候都不能赋值时,可以使用readonly修饰符修饰,当与其他修饰符同时出现时,其位置应该在改修饰符后面:

class Calendar extends Date {
  constructor(public readonly year: number) {
    super()
  }
}

const c: Calendar = new Calendar(2020)
c.year = 2019//Attempt to assign to const or readonly variable 复制代码


抽象类

传统面向对象编程语言中抽象类也是一个常见概念,下面是百度百科里面关于抽象类的介绍:

象类往往用来表征对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。

通常在编程语句中用 abstract 修饰的类是抽象类。在C++中,含有纯虚拟函数的类称为抽象类,它不能生成对象;在java中,含有抽象方法的类称为抽象类,同样不能生成对象。

在TypeScript中,抽象类不能够被实例化,只能够被实现或者继承,用abstract修饰;除此之外,属性和方法也能够被修饰:

abstract class MobileDevices {
  abstract monitor: string
  abstract CPU: string
  abstract GPU: string
  abstract RAM: string

  abstract powerOn()
}

class Phone extends MobileDevices {//

  constructor(
    public readonly CPU,
    public readonly GPU,
    public readonly RAM,
    public readonly monitor,
  ) {
    super()
  }

  powerOn() {
    console.log('按下开机键,缓缓响起开机音乐。两分钟之后。。。开机成功')
  }

}

const md = new MobileDevices()//Cannot create an instance of an abstract class.
const nokiaElderlyPhone = new Phone('ARM11', '无', '512kb', '2寸大显示屏')
nokiaElderlyPhone.powerOn()复制代码

或者phone类实现MobileDevices:

class Phone implements MobileDevices {

  constructor(
    public readonly CPU,
    public readonly GPU,
    public readonly RAM,
    public readonly monitor,
  ) {
  }

  powerOn() {
    console.log('按下开机键,缓缓响起开机音乐。两分钟之后。。。开机成功')
  }

}复制代码


类作为类型

类定义除了能够让子类继承、实现,被实例化之外,还能够当做类型使用,其中用和接口使用方式类似:

const md: MobileDevices = {
  CPU: '',
  GPU: '',
  RAM: '',
  monitor: '',
  powerOn() {
  }

}
//下面的写法等同于上面的写法
const nokiaElderlyPhone: Phone = {
  CPU: '',
  GPU: '',
  RAM: '',
  monitor: '',
  powerOn(): any {
  }
}复制代码

类与接口

上面我们讲了Class的基本用法,下面我们看看接口与类之间的那些特殊用法。

接口继承类

前面讲到,接口能够继承接口,但是你绝对想不到,接口还能继承类(继承了JavaScript的灵活性)

image.png

class Glass {
  capacity: string
  shape: string
  size: string

  load(food: string) {
    console.log(`装入${food}`)
  }
}

interface MugGlass extends Glass {
  CupHandleShape: string
}

const mugGlass: MugGlass = {
  CupHandleShape: '',
  capacity: '',
  shape: '',
  size: '',
  load(food: string): void {
  }
}复制代码


类实现接口

既然类能实现类,那类能不能实现接口呢?答案是可以的,并且同时可以实现多个接口

image.jpeg

interface GlassInterface {
  capacity: string
  shape: string
  size: string
  load: (food: string) => void
}

interface Structure {
  bodyShape: string;
  lidShape: string;
}

class MugGlass implements GlassInterface, Structure {
  bodyShape: string
  lidShape: string
  capacity: string
  shape: string
  size: string
  CupHandleShape: string

  load(food: string): void {
  }
}

const mugGlass: MugGlass = {
  bodyShape: '',
  lidShape: '',
  CupHandleShape: '',
  capacity: '',
  shape: '',
  size: '',
  load(food: string): void {
  }
}复制代码


泛型

介绍

泛型是什么?下面这段话摘自维基百科的泛型

泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。AdaDelphiEiffelJavaC#F#SwiftVisual Basic .NET 称之为泛型(generics);MLScalaHaskell 称之为参数多态(parametric polymorphism);C++D称之为模板。具有广泛影响的1994年版的《Design Patterns》一书称之为参数化类型(parameterized type)。


TypeScript虽然是强类型约束的,但是同时又继承了JavaScript的灵活性,any和本章中的泛型就是其灵活性的体现。我们通过一个简单的例子来认识下泛型:

function createArray<T>(length: number, defaults: any): Array<T> {
  const arr: Array<T> = []
  for (let i = 0; i < length; i++) {
    arr.push(defaults)
  }
  return arr
}

const numArr = createArray<number>(20, 0)复制代码

上面代码中,我们通过调用方法时指定Tnumber类型(T只是一个类型变量,可以用其他字母代替,推荐使用单个大写字母定义),可以在函数作用域内使用该类型T,而TypeScript也会根据调用时指定的额泛型类型制动推断numArr的类型:

image.png

也可以定义多个泛型类型,如下面的例子:

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]复制代码


约束泛型类型

泛型尽管很灵活,但是由于没有制定具体类型,TypeScript无法自动推断类型,这时候我们就可以指定泛型有哪些属性:

async function god2BaiduAsync<T extends number>(gdLon: T, gdLat: T, wait = 0): Promise<[T, T]> {
  return new Promise(resolve => {
    setTimeout(() => {
      let bdLatLon: [T, T] = [gdLon, gdLat]
      let PI = 3.14159265358979324 * 3000.0 / 180.0
      let x = gdLon
      let y = gdLat
      let z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * PI)
      let theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * PI)
      bdLatLon[0] = <T>(z * Math.cos(theta) + 0.0065)//泛型类型断言,这里面如果不断言为T将会报错
      bdLatLon[1] = <T>(z * Math.sin(theta) + 0.006)
      resolve(bdLatLon)
    }, wait)
  })
}

//Argument of type '"113.97471"' is not assignable to parameter of type 'number'.
const baiduPosition = god2BaiduAsync<number>('113.97471', '22.660848')

const baiduPosition2 = god2BaiduAsync<number>(113.97471, 22.660848)复制代码

上面将高德地图经纬度转换为百度地图经纬度将泛型定义为number,调用时,如果不是number类型将会报错,同时对于有些情况下需要用泛型进行类型断言(类型断言在后面会讲到)。

泛型接口

之前我们写过一个

createArray

函数,如果我们用接口+泛型来定义其形状呢:

interface CreateArray {
  <T>(length: number, defaults: any): Array<T>
}

const createArray: CreateArray = <T>(length: number, defaults: any): Array<T> => {
  const arr: Array<T> = []
  for (let i = 0; i < length; i++) {
    arr.push(defaults)
  }
  return arr
}

const numArr = createArray<number>(20, 0)复制代码

我们可以对CreateArray做一点点优化,把泛型提升到接口名上:

interface CreateArray<T> {
  (length: number, defaults: T): Array<T>
}

const createArray: CreateArray<any> = <T>(length: number, defaults: any): Array<T> => {
  const arr: Array<T> = []
  for (let i = 0; i < length; i++) {
    arr.push(defaults)
  }
  return arr
}

const numArr = createArray(20, 0)复制代码

上一篇介绍数组的时候我们介绍过定义数组是使用泛型方式定义一个数组,现在我们简单地手动实现一个泛型数组:

interface PseudoArray<T> {
  [index: number]: T
}

let numArr: PseudoArray<number> = [1, 3]
let strNum: PseudoArray<string> = new Array('1', '2', '3')
let bolArr:PseudoArray<boolean>=[1,3]//Type 'number' is not assignable to type 'boolean'.复制代码

泛型类

泛型除了可以用到接口中,还可以用到类中,让我们用class实现上面的泛型数组

class PseudoArray<T> {
  [index: number]: T

  constructor(...args) {
    return args
  }
}

let numArr: PseudoArray<number> = [1, 3]//[ 1, 3 ]
let strArr: PseudoArray<string> = new PseudoArray<string>('1', '2', '3')//[ '1', '2', '3' ]
let bolArr: PseudoArray<boolean> = new PseudoArray<string>('1', '2', '3')//Type 'PseudoArray<string>' is not assignable to type 'PseudoArray<boolean>'.   Type 'string' is not assignable to type 'boolean'.复制代码

泛型参数的默认值

我们可以为泛型设定默认类型:

function createArray<T = number>(length: number, defaults: any): Array<T> {
  const arr: Array<T> = []
  for (let i = 0; i < length; i++) {
    arr.push(defaults)
  }
  return arr
}复制代码

类型推论与类型兼容

类型推论

什么是类型推论?

TypeScript类型推论是怎么回事呢?TypeScript相信大家都很熟悉,但是TypeScript类型推论是怎么回事呢,下面就让小编带大家一起了解吧。

TypeScript类型推论,其实就是TypeScript内部实现了类型推论,大家可能会很惊讶TypeScript怎么会类型推论呢?但事实就是这样,小编也感到非常惊讶。

这就是关于TypeScript类型推论的事情了,大家有什么想法呢,欢迎在评论区告诉小编一起讨论哦!

好了,不皮了,放下你手中的啤酒瓶、板砖、狼牙棒,让我来好好说道说道

在TypeScript中,如果没定义类型则会自动根据所赋值内容自动推断类型,例如:

let num=1
num='1'//Type '"1"' is not assignable to type 'number'.复制代码

对于没有赋值的变量,则会自动推断为any类型:

let num
num = 1
num = '1'
num = false
num = true复制代码


最佳通用类型

当有多种类型的时候,TypeScript会使用这些类型来推断出一个最合适的类型:

let arr = [1, 2, true, '3']复制代码

如果你的编辑器对TypeScript支持比较友好的话,鼠标悬停在类型上面就会提示其类型推论的结果:

image.png

可以清晰地看到,TypeScript会推断为一个包含所有类型的联合类型

由于是选用所有类型作为其候选类型,所以当使用继承自相同父类型的子类型时,可能会出现偏差:

interface Man extends Human {
  
}
interface Woman extends Human {
  
}

const epson: Man = {
}

const marry: Woman = {
}
const peoples=[epson,marry]复制代码

这里类型推断为Array<Man>:

image.png

这时候我们可以手动指定其父类型作为其最合适的类型:

const peoples:Array<Human>=[epson,marry]复制代码


类型兼容

TypeScript的类型兼容性是基于结构子类型的,且不要求明确声明。


结构类型是一种只使用其成员来描述类型的方式,与名义类型形成对比。

名义类型:数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定,名义数据类型语言(c#、java)

对象间的兼容

对象间的兼容比较简单,假如有两个对象:a、b,a要兼容b对象,则b至少需要与a相同的属性(相同的名称和类型,可比a多额外属性),例如:动物能够兼容人类(动物中包含了人类),但是人类不能兼容动物(不能说人类包含了动物)。

interface Computer {
  GPU: string;
  CPU: string;
  RAM: string;
}

interface Phone {
  GPU: string;
  CPU: string;
  RAM: string;
  screen: string;
}

let myComputer:Computer={
  CPU: '线程撕裂者',
  GPU: 'RTX2080Ti',
  RAM: '1T'
}

let myPhone:Phone={
  CPU: '麒麟1000',
  GPU: '无',
  RAM: '16G',
  screen: '三星'
}

myComputer=myPhone复制代码


函数间的兼容

函数的兼容与对象有所不同,例如:x能够赋值给y,则x参数类型并须按顺序出现在y的参数列表里:

let x = (n: number) => n
let y = (a: number, b: string) => a
y = x
x=y//Type '(a: number, b: string) => number' is not assignable to type '(n: number) => number'.复制代码

这里y有两个参数,而x只有一个参数,TypeScript允许忽略其余参数,沿用了JavaScript的一贯做法,如:Array.filter(element:any,index?:number,array?:Array<any>),其中只有element是必须的,其余参数都可忽略


而对于返回值,其遵循对象兼容规则,例如:y的返回值需要兼容x的返回值,则x返回值至少需要包含y的返回值的属性:

let x: () => { n: number, s: string } = () => ({n: 1, s: ''})
let y: () => { n: number } = () => ({n: 1})
y = x
x = y// Type '() => { n: number; }' is not assignable to type '() => { n: number; s: string; }'.   Property 's' is missing in type '{ n: number; }' but required in type '{ n: number; s: string; }'.复制代码


枚举

不同枚举间即使值相同,也不能够兼容;但是枚举类型和数字类型能够相互兼容:

enum Duirection {
  right,
  left,
  bottom,
  top
}

enum Color {
  red,
  green,
  blue,
  none
}

enum MobileDevices {
  phone = 'phone',
  computer = 'computer',
  watch = 'watch'
}

let d = Duirection.top
d = 1
d = Color.red//Type 'Color.red' is not assignable to type 'Duirection'.

let md = MobileDevices.phone
md = 'computer'//Type '"computer"' is not assignable to type 'MobileDevices'.

const num: number = Duirection.top复制代码


类与字面量的兼容差不多,有一点不同:类有静态部分和实例部分的类型。

比较两个类类型变量时,只有实例成员才会被比较,静态成员和构造函数不在比较范围内。

class Animal {
  feet: number;
  constructor(name: string, numFeet: number) { }
}

class Size {
  feet: number;
  constructor(numFeet: number) {
  }
}

let a: Animal;
let s: Size;

a = s;  // OK
s = a;  // OK复制代码


高级类型

前面介绍的那些类型,在开发中都够用了,但是,还有一些高级类型,用的比较少,但也有些场合需要用到

交叉类型

交叉类型听名字像是某几个类型的交集,其实是某几个类型的并集,交叉类型的场景:Mixins等

interface AnyObj {
  [pro: string]: any
}

function extend<F extends AnyObj, S extends AnyObj>(first: F, second: S): F & S {
  const result: Partial<F & S> = Object.assign({}, first, second)
  return <F & S>result
}

interface Person {
  name: string,
  age: number
}

interface Ordered {
  serialNo: number,

  getSerialNo(): number
}

const personA: Person = {
  name: 'Jim',
  age: 20
}

const orderOne: Ordered = {
  serialNo: 1,
  getSerialNo() {
    return this.serialNo
  }
}
const personOrderd = extend(personA, orderOne)复制代码

联合类型

联合类型指定数据可能是某几种类型中的其中一种。

联合类型在实际的应用中很常见,比如某个对象,他的值可能是个元组,也可能是这个类型本身:

interface InfoData {
  [name: string]: {
    [name: string]: [string, string] | InfoData
  }
}复制代码


类型别名

类型别名用于给类型起个新名字,能够为原始值、原始数据类型、联合类型、元组、交叉类型等其他任何需要手写的类型:


定义一个狮虎兽接口:

interface Lion {
  family: string;
  color: string;
  maneColor: string;
  status: string;
  age: number;
}

interface Tiger {
  stripe: string;
  swimmingSpeed: number;
  treeClimbingSpeed: number;
}

type Liger = Lion & Tiger复制代码


提示类型:

type MessageType = 'success' | 'info' | 'warning' | 'error'复制代码


或者交叉类型的联合类型:

type MessageType = 'success' | 'info' | 'warning' | 'error'
type UserRole = 'admin' | 'master' | 'tourist'
type types = MessageType | UserRole复制代码

类型断言

类型单元有两种用法

1.利用as断言

值 as 类型复制代码

2.泛型断言

<类型>值复制代码


有一个例子:

interface Cat {
  name: string;

  climbing(): void
}

interface Fish {
  name: string;

  swim(): void
}

function isCat(animal: Cat | Fish) {
  //Property 'climbing' does not exist on type 'Cat | Fish'.   Property 'climbing' does not exist on type 'Fish'.
  if (animal?.climbing ?? false) {
    return true
  }
  return false
}

const fish: Fish = {
  name: '',
  swim(): void {
  }
}
console.log(isCat(fish))复制代码

可以看到isCat方法中animal?.climbing报错,因为TypeScript不清楚animal的类型到底是Cat还是Fish,要想解决这个问题,我们可以断言animal类型为Cat:

function isCat(animal: Cat | Fish) {
  if ((animal as Cat)?.climbing ?? false) {
    return true
  }
  return false
}复制代码

断言的用途

  • 将联合类型断言为其中一种类型
  • 父类断言为更加具体的子类
  • any断言为具体的类型

声明合并

对于相同名称的函数、类、接口的声明,TypeScript会合并为一个函数、类、接口

接口

话不多说,直接上例子:

interface Book {
  long: string;
  wide: string;
  thickness: string;
}

interface Book {
  pageNumber: number;
  author: string;
  press: string;
}

const book: Book = {
  author: '',
  long: '',
  pageNumber: 0,
  press: '',
  thickness: '',
  wide: ''
}复制代码

可以看到,虽然定义了两个Book接口,但是没有报错,而且将对象声明为Book类型时,该对象有两个Book接口的所有属性。

函数

函数的合并参考函数重载

声明文件

介绍

声明文件常用于没有提供类型声明的第三方库和项目的一些自定义功能(比如自定义全局工具类、扩展框架的功能等)。而类型声明提供的编码提示体验,也正是让TypeScript火起来的原因之一。


作为非库开发者,我们只需要浅浅的学习声明全局变量、扩展框架、扩展库的声明即可。


在了解学习声明文件之前,我们需要了解两个概念:

  • 声明语句
  • 声明文件

什么是声明语句?

声明语句简单来说就是对于变量、函数、类型的声明,让TypeScript认识他们。

如通过CDN的形式引入JQuery,为了让TypeScript识别出$,我们可以这样写:

declare function $(selector: string): $复制代码

上面代码的意思是,声明一个全局函数$他的参数selector类型是string类型,然后再返回$本身(JQuery正事通过返回JQuery对象本身实现链式调用)

什么是声明文件?

声明文件包含很多声明语句,其后缀为.d.ts,声明文件作用是编码提示和定义类型,编译阶段会删除

拿大名鼎鼎的JQuery举例,如果我们通过CDN的形式引入,TypeScript是不知道有JQuery的,这时候有两种选择:

  1. 下载社区提供的第三方声明文件(@type文件)
  2. 手写声明文件

这里我们选择简单地手写一下JQuery的声明文件,新建一个JQuery.d.ts文件,我们需要用到他的选择器功能和hide函数:

// @ts-ignore
declare function $(selector: string): $

declare namespace $ {
  function hide(duration?: number | string, complete?: Function)
  function hide(option: {
    duration?: number | string,
    easing?: string,
    queue?: boolean | string,
    specialEasing?: object,
    setp?: Function,
    progress?: Function,
    complete?: Function,
    done?: Function,
    fail?: Function,
    always?: Function
  })
}复制代码

这时候TypeScript就能够正常使用.hide方法了

const inner = $('#inner')
setTimeout(() => {
  inner.hide()
}, 2000)复制代码

声明全局变量

全局变量通过declare定义,如果声明文件中出现exportimport关键字,则会认为是UMD包的生命形式,由全局变为局部(只在当前文件作用域中有效)

全局变量

全局变量可以通过declare vardeclare letdeclare const来定义全局变量和常量。

declare vardeclare let定义全局变量,理论上没有什么区别,但是我们项目中使用ES6+,所以建议使用declare let定义:

declare let JQuery:(selector:string)=>any复制代码

使用declare const定义全局常量,不可更改,相较于declare let,更推荐用declare const(全局变量允许直接修改的情况很少,一般都是常量):上面的例子改造一下:

declare const JQuery:(selector:string)=>any复制代码

全局方法

使用declare function声明一个全局函数,函数能够使用重载,如刚刚的JQuery的$定义:

// @ts-ignore
declare function $(): $
// @ts-ignore
declare function $(element: Element): $
// @ts-ignore
declare function $(object: Object): $
// @ts-ignore
declare function $(selector: string, content: Element | $): $复制代码

其代码提示是这样:image.png

全局类

当变量是一个全局类时,可以通过declare class定义,比如通过CDN引入router时:

declare class VueRouter {

}复制代码

全局类型

全局类型可以用interface或者type直接定义全局类型:

//index.d.ts
interface Pen {
  size: string;
}

//index.ts
const pen: Pen = {
  size: ''
}复制代码

全局枚举

使用declare enum定义全局枚举(又称外部枚举,只用来定义类型,不编译实际内容):

//index.d.ts
declare enum Direction {
  up,
  right,
  down,
  left
}

//index.ts
console.log(Direction.up)// ReferenceError: Direction is not defined复制代码

全局对象

使用declare namespace定义全局变量(命名空间),变量内并直接使用letconstenuminterfacefunction等关键字进行声明,如JQuery的声明:

declare namespace $ {
  const jquery:string
  function hide(duration?: number | string, complete?: Function)
  function hide(option: {
    duration?: number | string,
    easing?: string,
    queue?: boolean | string,
    specialEasing?: object,
    setp?: Function,
    progress?: Function,
    complete?: Function,
    done?: Function,
    fail?: Function,
    always?: Function
  })
}复制代码

也可以在内部声明对象:

declare namespace $ {
  const jquery:string
  namespace fn{
    const a:string
  }
}复制代码

扩展全局变量

在日常项目中,我们可能需要扩展全局变量,比如给window对象添加了一个refresh方法,他调用location.reload()方法:

<script>
  window.refresh = () => {
    location.reload()
  }
</script>复制代码

我们声明文件种可以用interface对Window对象进行扩展:

//index.d.ts
interface Window {
  refresh(): void
}
//index.ts
setTimeout(()=>{
  window.refresh()
},2000)复制代码

或者用declare namespace给全局命名空间添加refresh方法声明:

//index.d.ts
declare namespace Global {
  function refresh(): void
}
//index.ts
setTimeout(()=>{
  refresh()
},2000)复制代码


扩展现有模块

扩展模块的语法为:declare module


实际项目中,需要扩展现有模块的情况非常多,比如在Vue项目中,我们使用了css module并且定义了一个全局插件Toast,为Vue实例添加了一个方法$toast,我们可以这样写声明文件:

//扩展vue/types/vue.d.ts所暴露出来的模块
declare module 'vue/types/vue' {
  interface Vue {
    $style: {
      [key: string]: string;
    };
  }

  interface VueConstructor {
    $style: {
      [key: string]: string;
    };
    $toast(option: Option): void;
  }
}复制代码

结语

本章内容对于没有接触过如Java、C++等强类型语言的同学来说,可能会有一点点难,这些东西在实际项目中也是很常用的东西,建议是多做小例子练习。


文章写得可能不全面、不对或者有难理解的地方,欢迎在评论区踊跃发言哦😊😊

同时,各位大佬喷的轻一点,毕竟:


下一篇将介绍TypeScript在项目中如何应用,我是八字昵称,我们下篇见