一块儿学 TypeScript 基础篇

今年10月初尤雨溪在 GitHub 发布了 vue3 的 Pre-Alpha 版本源码,同时大部分源码使用了 TypeScript 语言进行编写。能够说 TypeScript 已经成为前端开发将来的趋势。javascript

本篇大部份内容讲 TypeScript 的基础知识,后续内容会更新介绍 TypeScript 在工做中的项目开发及运用。若是您想要获得最新的更新,能够点击下面的连接:html

TypeScript开发教程 文档版前端

TypeScript开发教程 GitHubvue

什么是 TypeScript

TypeScript 是一种由微软开发的自由和开源的编程语言,它是 JavaScript 的一个超集,扩展了 JavaScript 的语法。java

安装 TypeScript

经过 npm 安装webpack

$ npm install typescript -g
复制代码

以上命令会在全局环境下安装 tsctsserver 两个命令,安装完成以后,咱们就能够在任何地方执行它了。git

tsserver

TypeScript 独立服务器(又名 tsserver )是一个节点可执行文件,它封装了 TypeScript 编译器和语言服务,并经过 JSON 协议公开它们。tsserver 很是适合编辑器和 IDE 支持。es6

通常工做中不经常使用到它。进一步了解tsservergithub

tsc

tsc 为 typescript compiler 的缩写,即 TypeScript 编译器,用于将 TS 代码编译为 JS 代码。使用方法以下:web

$ tsc index.ts
复制代码

编译成功后,就会在相同目录下生成一个同名 js 文件,你也能够经过命令参数来修改默认的输出名称。

默认状况下编译器以 ECMAScript 3(ES3)为目标。能够经过 tsc -h 命令查看相关帮助,能够了解更多的配置。

咱们约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀。

Hello TypeScript

结合 tsc 命令,咱们一块儿写一个简单的例子。

建立一个 index.ts 文件。

let text: string = 'Hello TypeScript'
复制代码

执行 tsc index.ts 命令,会在同目录下生成 index.js 文件。

var text = 'Hello TypeScript';
复制代码

一个简单的例子就实现完了。咱们能够经过官网提供的 Playground 进行验证。

可是在项目开发过程当中咱们会结合构建工具,如 webpack,和对应的本地服务 dev-server 等相关工具一同使用。

接下来把咱们了解到的知识结合在一块儿。搭建一个完整的项目

项目根目录中有一个 tsconfig.json 文件,简单介绍其做用。

tsconfig.json

若是一个目录下存在一个 tsconfig.json 文件,那么它意味着这个目录是 TypeScript 项目的根目录。tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。 一个项目能够经过如下方式之一来编译:

  • 不带任何输入文件的状况下调用 tsc,编译器会从当前目录开始去查找 tsconfig.json文 件,逐级向上搜索父目录。
  • 不带任何输入文件的状况下调用 tsc,且使用命令行参数 --project(或 -p )指定一个包含 tsconfig.json 文件的目录。

当命令行上指定了输入文件时,tsconfig.json文件会被忽略。

基础类型

TypeScript 支持与 JavaScript 几乎相同的数据类型。

JavaScript 数据类型

String、Number、Boolean、Object(Array、Function)、Symbol、undefined、null

TypeScript 新增数据类型

void、any、never、元组、枚举、高级类型

类型注解

做用:至关于强类型语言中的类型声明

语法:(变量/函数): type

介绍

字符串类型

咱们使用 string 表示文本数据类型。 和 JavaScript 同样,可使用双引号 " 或单引号 ' 表示字符串, 反引号 ` 来定义多行文本和内嵌表达式。

let str: string = 'abc'
复制代码

数字类型

和 JavaScript 同样,TypeScript 里的全部数字都是浮点数。这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015 中引入的二进制和八进制字面量。

let decLiteral: number = 6
let hexLiteral: number = 0xf00d
let binaryLiteral: number = 0b1010
let octalLiteral: number = 0o744
复制代码

布尔类型

咱们使用 boolean 表示布尔类型,表示逻辑值 true / false

let bool: boolean = true
复制代码

数组类型

TypeScript 有两种定义数组的方式。 第一种,能够在元素类型后加上 []。 第二种,可使用数组泛型 Array<元素类型>。 此外,在元素类型中可使用联合类型。 符号 | 表示或。

let arr1: number[] = [1, 2, 3]
let arr2: Array<number> = [1, 2, 3]
let arr3: Array<number | string> = [1, 2, 3, 'a']
复制代码

元组

元组类型用来表示已知元素数量和类型的数组,各元素的类型没必要相同,对应位置的类型必须相同。

let tuple: [number, string] = [0, '1']
tuple = ['1', 0] // Error
复制代码

当访问一个已知索引的元素,会获得正确的类型:

tuple[0].toFixed(2)
tuple[1].toFixed(2) // Error: Property 'toFixed' does not exist on type 'string'.
复制代码

能够调用数组 push 方法添加元素,但并不能读取新添加的元素。

tuple.push('a')
console.log(tuple) // [0, "1", "a"]
tuple[2] // Error: Tuple type '[number, string]' of length '2' has no element at index '2'.
复制代码

枚举

咱们使用 enum 表示枚举类型。 枚举成员值只读,不可修改。 枚举类型是对 JavaScript 标准数据类型的一个补充。C# 等其它语言同样,使用枚举类型为一组数值赋予友好的命名。

数字枚举

初始值为 0, 逐步递增,也能够自定义初始值,以后根据初始值逐步递增。

enum Role {
  Reporter = 1,
  Developer,
  Maintainer,
  Owner,
  Guest
}

console.log(Role.Developer) // 2
console.log(Role[2]) // Developer
复制代码

数字枚举会反向映射,能够根据索引值反向得到枚举类型。缘由以下编译后代码所示:

var Role;
(function (Role) {
    Role[Role["Reporter"] = 1] = "Reporter";
    Role[Role["Developer"] = 2] = "Developer";
    Role[Role["Maintainer"] = 3] = "Maintainer";
    Role[Role["Owner"] = 4] = "Owner";
    Role[Role["Guest"] = 5] = "Guest";
})(Role || (Role = {}));
复制代码

字符串枚举

字符串枚举不支持反向映射

enum Message {
  Success = '成功',
  Fail = '失败'
}
复制代码

常量枚举

在枚举关键字前添加 const,该常量枚举会在编译阶段被移除。

const enum Month {
  Jan,
  Feb,
  Mar
}
let month = [Month.Jan, Month.Feb, Month.Mar]
复制代码

编译后:

"use strict";
var month = [0 /* Jan */, 1 /* Feb */, 2 /* Mar */]; // [0 /* Jan */, 1 /* Feb */, 2 /* Mar */]
复制代码

外部枚举

外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型。

declare enum Month {
  Jan,
  Feb,
  Mar
}
let month = [Month.Jan, Month.Feb, Month.Mar]
复制代码

编译后:

"use strict";
let month = [Month.Jan, Month.Feb, Month.Mar];
复制代码

declare 定义的类型只会用于编译时的检查,编译结果中会被删除。因此按照上述例子编译后的结果来看,显然是不能够的。由于 Month 未定义。

  • declareconst 能够同时存在

对象

TypeScript 有两种定义对象的方式。 第一种,能够在元素后加上 object。 第二种,可使用 { key: 元素类型 } 形式。 一样在元素类型中可使用联合类型。注意第一种形式对象元素为只读。

let obj1: object = { x: 1, y: 2 }
obj1.x = 3 // Error: Property 'x' does not exist on type 'object'.

let obj2: {  x: number, y: number } = { x: 1, y: 2 }
obj2.x = 3
复制代码

Symbol

symbol 类型的值是经过 Symbol 构造函数来建立

let s: symbol = Symbol()
复制代码

Null & Undefined

null 表示对象值缺失,undefined 表示未定义的值。

let un: undefined = undefined
let nu: null = null
复制代码

若其余类型须要被赋值为 nullundefined 时, 在 tsconfig.json 中将 scriptNullChecks 设置为 false。或者 使用联合类型。

void

用于标识方法返回值的类型,表示该方法没有返回值。

function noReturn (): void {
  console.log('No return value')
}
复制代码
  • undefined 并非保留字段能够被赋值,因此设置undefined时,建议使用 void 0

任意类型

声明为 any 的变量能够赋予任意类型的值。

let x: any
x = 1
x = 'a'
x = {}

let arr: any[] = [1, 'a', null]
复制代码

函数

咱们先回顾在 JavaScript 中,使用 es6 语法定义一个函数。

let add = (x, y) => x + y
复制代码

上面例子中,add 函数有两个参数 xy 返回其相加之和。 该例子放在 TypeScript 中会提示 参数 xy 隐含一个 any 类型。 因此咱们修改以下:

let add = (x: number, y: number): number => x + y
复制代码

给参数添加 number 类型,在括号以后也添加返回值的类型。这里返回值类型能够省略,由于 TypeScript 有类型推断机制,这个咱们以后详细介绍。

接下来咱们使用 TypeScript 定义一个函数类型并实现它。

let plus: (x: number, y: number) => number

plus = (a, b) => a + b

plus(2, 2) // 2
复制代码

never

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些老是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;变量也多是 never 类型,当它们被永不为真的类型保护所约束时。

never 类型是任何类型的子类型,也能够赋值给任何类型;然而,没有类型是 never 的子类型或能够赋值给 never 类型(除了 never 自己以外)。 即便 any 也不能够赋值给 never

let error = (): never => {
    throw new Error('error')
}
let endless = (): never => {
    while(true) {}
}
复制代码
  • 类型推断:变量在声明时并未赋值,类型推断为 any

接口

在 TypeScript 中,咱们可使用接口 interface 来定义对象类型。

介绍

接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,须要由具体的类去实现,而后第三方就能够经过这组抽象方法调用,让具体的类执行具体的方法。

接下来,定义一个简单的接口:

interface Person {
  name: string
  age: number
}

let man: Person = {
  name: 'James',
  age: 30
}
复制代码

咱们定义了一个接口 Person 和变量 man,变量的类型是 Person。 这样咱们就约束了该变量的值中对象的 keyvalue 要和接口一致。

须要注意的是:

  1. 接口规范首字母大写;
  2. 被赋值的变量必须和接口的定义保持一致,参数不能多也不能少;
  3. 类型检查器不会去检查属性的顺序,只要相应的属性存在而且类型正确便可。

可选属性

接口的全部属性可能都不是必需的。

interface Person {
  name: string
  age?: number
}

let man: Person = {
  name: 'James'
}
复制代码

只读属性

属性名前使用 readonly 关键字制定为只读属性,初始化后不可更改。

interface Person {
  readonly name: string
  age: number
}

let man: Person = {
  name: 'James',
  age: 30
}

man.name = 'Tom' // Error: Cannot assign to 'name' because it is a read-only property.
复制代码

任意属性

用任意的字符串索引,使其能够获得任意的结果。

interface Person {
  name: string
  age: number
  [x: string]: any
}

let man: Person = {
  name: 'James',
  age: 30,
  height: '180cm'
}
复制代码

除了 nameage 必须一致之外,其余属性能够随意定义数量不限。

  • 一旦定义了任意属性,那么其余属性的类型必须是任意属性类型的子集。
interface Person {
  name: string
  age: number
  [x: string]: string
}

let man: Person = {
  name: 'James',
  age: 30,
  height: '180cm'
}

/** * Type '{ name: string; age: number; height: string; }' is not assignable to type 'Person'. * Property 'age' is incompatible with index signature. * Type 'number' is not assignable to type 'string'. */
复制代码

数字索引

能够获得任意长度的数组。

interface StringArray {
  [i: number]: string
}
let chars: StringArray = ['a', 'b']
复制代码

接口可以描述 JavaScript 中对象拥有的各类各样的外形。 除了描述带有属性的普通对象外,接口也能够描述对象类型函数类型

对象类型接口

示例以下:

interface List {
  readonly id: number
  name: string
  age?: number
}

interface Result {
  data: List[]
}

function render (result: Result) {
  console.log(JSON.stringify(result))
}
复制代码

首先咱们定义了一个 List 对象接口,它的内部有 idnameage 属性。接下来咱们又定义了一个对象接口,这个对象接口有只一个属性 data,它类型为 List[]。接下来有一个函数,参数类型为 Result

接下来咱们定义一个变量 result,将它传入 render 函数。

let result = {
  data: [
    { id: 1, name: 'A', sex: 'male' },
    { id: 2, name: 'B' }
  ]
}

render(result)
复制代码

这里须要注意 data 数组内的第一个对象里,增长了一个 sex 属性,可是在上面的接口定义中没有 sex 属性。这时把对象赋给 result 变量,传入函数,不会被编译器检查到。

再看下面的例子:

render({
  data: [
    { id: 1, name: 'A', sex: 'male' },
    { id: 2, name: 'B' }
  ]
})
// Error: Object literal may only specify known properties, and 'sex' does not exist in type 'List'.
复制代码

咱们将对象字面当作参数传给了 render 函数时,编译器会对对象内的属性进行检查。

咱们能够经过类型断言规避这个问题

render({
  data: [
    { id: 1, name: 'A', sex: 'male'},
    { id: 2, name: 'B' }
  ]
} as Result)
复制代码

除了使用 as 关键字,还能够用 <> 符号:

render(<Result>{
  data: [
    { id: 1, name: 'A', sex: 'male'},
    { id: 2, name: 'B' }
  ]
})
复制代码

函数类型接口

为了使用接口表示函数类型,咱们须要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每一个参数都须要名字和类型。

在数据类型中咱们提到过,能够用一个变量声明一个函数类型。

let add: (x: number, y: number) => number
复制代码

此外,咱们还能够用接口来定义它。

interface Add {
  (x: number, y: number): number
}

let add: Add = (a, b) => a + b
复制代码

除此以外,还有一种更简洁的方式就是使用类型别名

类型别名使用 type 关键字

type Add = (x: number, y: number) => number

let add: Add = (a, b) => a + b
复制代码
  • interface 定义函数(Add)和用 type 定义函数(Add)有区别?

typeinterface 多数状况下有相同的功能,就是定义类型。 但有一些小区别:
type:不是建立新的类型,只是为一个给定的类型起一个名字。type还能够进行联合、交叉等操做,引用起来更简洁。
interface:建立新的类型,接口之间还能够继承、声明合并。建议优先使用 interface

函数

和 JavaScript 同样,TypeScript 函数能够建立有名字的函数或匿名函数,TypeScript 为 JavaScript 函数添加了额外的功能,让咱们能够更容易的使用它。

在基本类型和接口部分中多多少少提到过函数,接下来总结四种定义函数的方式。

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

const add: (x: number, y: number) => number

type add = (x: number, y: number) => number

interface add {
  (x: number, y: number) => number
}
复制代码

TypeScript 里的每一个函数参数都是必要的。这里不是指不能把 nullundefined 当作参数,而是说编译器检查用户是否为每一个参数都传入了值。也就是说,传递给一个函数的参数个数必须与函数指望的参数个数保持一致。咱们举个例子:

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

add(1, 2) // Error: Expected 3 arguments, but got 2.
复制代码

在上述例子中,函数定义了3个参数,分别为 xyz,结果返回 xy 的和。并无使用参数 z,调用 add 只传入 xy 的值。这时 TypeScript 检查机制提示预期为三个参数,但实际只传入两个参数的错误。如何避免这种状况呢?

可选参数

在 TypeScript 里咱们能够在参数名旁使用 ? 实现可选参数的功能。

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

add(1, 2)
复制代码

通过修改,参数 z 变为可选参数,检查经过。

  • 可选参数必须在必选参数以后

默认参数

与 JavaScript 相同,在 TypeScript 里函数参数一样能够设置默认值,用法一致。

function add (x: number, y = 2) {
  return x + y
}
复制代码

根据类型推断机制,参数 y 为推断为 number 类型。

剩余参数

与 JavaScript 相同。TypeScript 能够把全部参数收集到一个变量里。

function add (x: number, ...rest: number[]) {
  return x + rest.reduce((prev, curr) => prev + curr)
}

add(1, 2, 3) // 6
复制代码
  • 剩余参数必须在必选参数以后,可选参数不容许和剩余参数共同出如今一个函数内。

函数重载

TypeScript 的函数重载,要求咱们先定义一系列名称相同的函数声明。

function add (...rest: number[]): number
function add (...rest: string[]): string
function add (...rest: any[]): any {
  let first = rest[0]
  let type = typeof first
  switch (type) {
    case 'number':
      return rest.reduce((prev, curr) => prev + curr)
    case 'string':
      return rest.join('')
  }
  return null
}
复制代码

上面例子中,咱们定义了三个相同名称的函数,参数分别为 numberstringany 类型数组,相继返回的类型与参数类型相同。当调用该函数时,TypeScript 编译器可以选择正确的类型检查。在重载列表里,会从第一个函数开始检查,从上而下,因此咱们使用函数重载时,应该把最容易用到的类型放在最上面。

  • any 类型函数不是重载列表的一部分

传统的 JavaScript 使用函数和基于原型的继承来建立可重用的组件。

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)
复制代码

从 ES6 开始,咱们可以使用基于类的面向对象的方式。

class Point {
  constructor (x, y) {
    this.x = x
    this.y = y
  }
  toString () {
    return `(${this.x}, ${this.y})`
  }
}
复制代码

TypeScript 除了保留了 ES6 中类的功能之外,还增添了一些新的功能。

class Dog {
  constructor (name: string) {
    this.name = name
  }
  name: string
  run () {}
}

class Husky extends Dog {
  constructor (name: string, color: string) {
    super(name)
    this.color = color
  }
  color: string
}
复制代码

上面的例子中须要注意如下几点:

  1. 继承类中的构造函数里访问 this 的属性以前,必定要调用 super 方法;
  2. TypeScript 和 ES6 中,“类的成员属性”都是实例属性,而不是原型属性,“类的成员方法”都是“原型”方法。Dog.prototype => {constructor: ƒ, run: ƒ}new Dog('huang') => {name: "huang"}
  3. TypeScript 中实例的属性必须有初始值,或者在构造函数中被初始化。

public、private、protected、readonly

TypeScript 可使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

  • public 修饰的属性或方法是公有的,能够在任何地方被访问到,默认全部的属性和方法都是 public

  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问,包括继承它的类也不能够访问

  • protected 修饰的属性或方法是受保护的,它和 private 相似,区别是它在子类中也是容许被访问

  • 以上三种能够修饰构造函数,默认为 public,当构造函数为 private 时,该类不容许被继承或实例化;当构造函数为 protected 时,该类只容许被继承。

  • readonly 修饰的属性为只读属性,只容许出如今属性声明或索引签名中。

public

公共修饰符

class Animal {
  public name: string
  public constructor (name: string) {
    this.name = name
  }
}

let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom'
console.log(a.name) // Tom
复制代码

private

私有修饰符

class Animal {
  private name: string
  public constructor (name: string) {
    this.name = name
  }
}

let a = new Animal('Jack')
console.log(a.name) // Error: Property 'name' is private and only accessible within class 'Animal'.

class Cat extends Animal {
  constructor (name: string) {
    super(name)
    console.log(this.name) // Error: // Property 'name' is private and only accessible within class 'Animal'.
  }
}
复制代码

须要注意的是,TypeScript 编译以后的代码中,并无限制 private 属性在外部的可访问性。

上面的例子编译后的代码以下:

var Animal = (function () {
    function Animal (name) {
        this.name = name
    }
    return Animal
}())
var a = new Animal('Jack')
console.log(a.name)
复制代码

protected

受保护修饰符

class Animal {
  protected name: string
  public constructor (name: string) {
    this.name = name
  }
}

class Cat extends Animal {
  constructor (name: string) {
    super(name)
    console.log(this.name)
  }
}
复制代码
  • 构造函数参数添加修饰等同于在类中定义该属性,这样使代码更为简洁。
class Animal {
  // public name: string
  constructor (public name: string) {
    this.name = name
  }
}
class Cat extends Animal {
  constructor (public name: string) {
    super(name)
  }
}

let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom'
console.log(a.name) // Tom
复制代码

readonly

只读修饰符

class Animal {
  readonly name: string
  public constructor (name: string) {
    this.name = name
  }
}

let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom' //Error: Cannot assign to 'name' because it is a read-only property.
复制代码

注意若是 readonly 和其余访问修饰符同时存在的话,须要写在其后面。

class Animal {
  // public readonly name: string
  public constructor (public readonly name: string) {
    this.name = name
  }
}
复制代码

抽象类

abstract 用于定义抽象类和其中的抽象方法。须要注意如下两点:

抽象类不容许被实例化

abstract class Animal {
  public name: string
  public constructor (name: string) {
    this.name = name
  }
}

var a = new Animal('Jack') //Error: Cannot create an instance of an abstract class.
复制代码

抽象类中的抽象方法必须被继承类实现

abstract class Animal {
  public name: string;
  public constructor (name: string) {
    this.name = name;
  }
  abstract sayHi (): any
}

class Cat extends Animal {
  public color: string
  sayHi () { console.log(`Hi`) }
  constructor (name: string, color: string) {
    super(name)
    this.color = color
  }
}

var a = new Cat('Tom', 'Blue')
复制代码

类与接口

本章节主要介绍类与接口之间实现、相互继承的操做。

类实现接口

实现(implements)是面向对象中的一个重要概念。通常来说,一个类只能继承自另外一个类,有时候不一样类之间能够有一些共有的特性,这时候就能够把特性提取成接口(interface),用 implements 关键字来实现。这个特性大大提升了面向对象的灵活性。

interface Animal {
  name: string
  eat (): void
}

class Cat implements Animal {
  constructor (name: string) {
    this.name = name
  }
  name: string
  eat () {}
}
复制代码
  • 类实现接口时,必须声明接口中全部定义的属性和方法。
interface Animal {
  name: string
  eat (): void
}

class Cat implements Animal {
  constructor (name: string) {
    this.name = name
  }
  name: string
  // eat () {}
}

// Error: Class 'Cat' incorrectly implements interface 'Animal'. Property 'eat' is missing in type 'Cat' but required in type 'Animal'.
复制代码
  • 类实现接口时,声明接口中定义的属性和方法不能修饰为 privateprotected
interface Animal {
  name: string
  eat (): void
}

class Cat implements Animal {
  constructor (name: string) {
    this.name = name
  }
  private name: string
  eat () {}
}

// Error: Class 'Cat' incorrectly implements interface 'Animal'. Property 'name' is private in type 'Cat' but not in type 'Animal'.
复制代码
  • 接口不能约束类中的构造函数
interface Animal {
  new (name: string): void
  name: string
  eat (): void
}

class Cat implements Animal {
  constructor (name: string) {
    this.name = name
  }
  name: string
  eat () {}
}

// Error: Class 'Cat' incorrectly implements interface 'Animal'. Type 'Cat' provides no match for the signature 'new (name: string): void'.
复制代码

接口继承接口

实现方法以下:

interface Animal {
  name: string
  eat (): void
}

interface Predators extends Animal {
  run (): void
}

class Cat implements Predators {
  constructor (name: string) {
    this.name = name
  }
  name: string
  eat () {}
  run () {}
}
复制代码
  • 继承多个接口用 , 分割,同理实现多个接口方式相同。
interface Animal {
  name: string
  eat (): void
}
  
interface Lovely {
  cute: number
}

interface Predators extends Animal, Lovely {
  run (): void
}

class Cat implements Predators {
  constructor (name: string, cute: number) {
    this.name = name
    this.cute = cute
  }
  name: string
  cute: number
  eat () {}
  run () {}
}
复制代码

接口继承类

实现方法以下:

class Auto {
  constructor (state: string) {
    this.state = state
  }
  state: string
}

interface AutoInterface extends Auto {}

class C implements AutoInterface {
  state = ''
}
复制代码

混合类型

interface SearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: SearchFunc
mySearch = function(source: string, subString: string) {
  return source.search(subString) !== -1
}
复制代码

一个函数还能够有本身的属性和方法

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

function getCounter(): Counter {
  let counter = <Counter>function (start: number) {}
  counter.interval = 123
  counter.reset = function () {}
  return counter
}

let c = getCounter()
c(10)
c.reset()
c.interval = 5.0
复制代码

小结

  1. 接口与接口、类与类之间能够相互继承(extends)
  2. 接口能够经过类来实现的(implements),接口只能约束类的公有成员
  3. 接口能够抽离出类的成员、包括公有(public)、私有(private)、受保护(protected)成员

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

  • 小技巧:直接把泛型理解为表明类型的参数

简单的例子

首先,咱们来实现一个函数 createArray,它能够建立一个指定长度的数组,同时将每一项都填充一个默认值:

function createArray(length: number, value: any): Array<any> {
  let result = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray(3, 'x') // ['x', 'x', 'x']
复制代码

这段代码编译不会报错,可是一个显而易见的缺陷是,它并无准确的定义返回值的类型:

Array<any> 容许数组的每一项都为任意类型。可是咱们预期的是,数组中每一项都应该是输入的 value 的类型。

这时候,泛型就派上用场了:

function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray<string>(3, 'x') // ['x', 'x', 'x']
复制代码

上例中,咱们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中便可使用了。

接着在调用的时候,能够指定它具体的类型为 string。固然,也能够不手动指定,而让类型推断自动推算出来:

function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray(3, 'x') // ['x', 'x', 'x']
复制代码

一样类型数组也能够被类型推断。

function log<T> (value: T): T {
  console.log(value)
  return value
}

log<string[]>(['a', 'b'])
// or
log(['a', 'b'])
复制代码

多个类型参数

定义泛型的时候,能够一次定义多个类型参数:

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

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

上例中,咱们定义了一个 swap 函数,用来交换输入的元组。

泛型约束

在函数内部使用泛型变量的时候,因为事先不知道它是哪一种类型,因此不能随意的操做它的属性或方法。

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length) // Error: Property 'length' does not exist on type 'T'.
  return arg
}
复制代码

上例中,泛型 T 不必定包含 length 属性,因此编译的时候会报错。

这时,咱们能够对泛型进行约束,只容许这个函数传入那些包含 length 属性的变量。这就叫泛型约束

interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}
复制代码

上例中,咱们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。

此时若是调用 loggingIdentity 函数的时候,传入的参数不包含 length,那么在编译阶段就会报错了。

interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}

loggingIdentity(7) // Error: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.
复制代码

多个类型参数之间也能够相互约束。

function copyFields<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
    target[id] = (<T>source)[id]
  }
  return target
}

let x = { a: 1, b: 2, c: 3, d: 4 }

copyFields(x, { b: 10, d: 20 }) // { a: 1, b: 10, c: 3, d: 20 }
复制代码

上述例子中,咱们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。

泛型函数

能够用泛型来约束函数的参数和返回值类型。

type Log = <T>(value: T) => T

let log: Log = (value) => {
  console.log(value)
  return value
}

log<number>(2) // 2
log('2') // '2'
log(true) // <boolean>true 
复制代码

泛型接口

以前学习过,可使用接口的方式来定义一个函数须要符合的形状。

interface SearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: SearchFunc
mySearch = function (source: string, subString: string) {
  return source.search(subString) !== -1
}
复制代码

一样也可使用含有泛型的接口来定义函数的形状。

interface CreateArrayFunc {
  <T>(length: number, value: T): Array<T>
}

let createArray: CreateArrayFunc
createArray = function<T>(length: number, value: T): Array<T> {
  let result: T[] = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray(3, 'x') // ['x', 'x', 'x']
复制代码

进一步,咱们能够把泛型参数提早到接口名上。

interface CreateArrayFunc<T> {
  <T>(length: number, value: T): Array<T>
}

let createArray: CreateArrayFunc<any>
createArray = function<T>(length: number, value: T): Array<T> {
  let result: T[] = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray(3, 'x') // ['x', 'x', 'x']
复制代码

注意,此时在使用泛型接口的时候,须要定义泛型的类型。

若不想在使用泛型接口时定义泛型的类型,那么,须要在接口名上的泛型参数设置默认类型。

interface CreateArrayFunc<T = any> {
  <T>(length: number, value: T): Array<T>
}

let createArray: CreateArrayFunc
复制代码

泛型类

与泛型接口相似,泛型也能够用于类的类型定义中。

class Log<T> {
  run (value: T) {
    console.log(value)
    return value
  }
}

let log1 = new Log<number>()
log1.run(1) // 1

let log2 = new Log()
log2.run('1') // '1'
复制代码
  • 注意: 泛型不能应用于类的静态成员。
class Log<T> {
  static run (value: T) {
    console.log(value)
    return value
  }
}
// Error: Static members cannot reference class type parameters.
复制代码

小结

  1. 函数和类能够轻松支持多种类型,加强程序的扩展性
  2. 没必要写多条函数重载,冗长的联合类型声明,加强代码可读性
  3. 灵活控制类型之间的约束

类型检查机制

TypeScript 编译器在作类型检查时,所秉承的一些原则,以及表现出的一些行为。

本章节分为三大部分:类型推断类型兼容性类型保护

类型推断

不须要指定变化的类型(函数的返回值类型),TypeScript 能够根据某些规则自动为其推断出一个类型。

基础类型推断

基本类型推断常常出如今初始化变量的时候。

let a
// let a: any

let a = 1
// let a: number

let a = []
// let a: any[]
复制代码

声明变量 a 时,咱们不指定它的类型,ts 就会默认推断出它是 any 类型。

若是咱们将它复制为 1ts 就会推断出它是 number 类型。

若是咱们将它复制为 []ts 就会推断出它是 any 类型的数组。

基本类型推断还会出如今定义函数参数。

let a = (x = 1) => {}
// let a: (x?: number) => void
复制代码

声明函数 a,设置一个参数 x,为它赋值一个默认参数 1,此时 ts 就会推断出它是 number 类型。一样返回值类型也会被推断。

最佳通用类型推断

当须要从多个类型中推断出一个类型时,ts 就会尽量的推断出一个最佳通用类型。

let a = [1, null]
// let a: (number | null)[]
复制代码

声明一个变量 a,值为一个包含数字 1null 的数组。此时,变量 a 就被推断为 numbernull 的联合类型。

以上的类型推断都是从右向左的推断,根据表达式的值推断出变量的类型。还有一种方式是从左到右,根据上下文推断。

上下文类型推断

一般发生在事件处理中。

window.onkeydown = (event) => {
}
// (parameter) event: KeyboardEvent
复制代码

window 绑定 onkeydown 事件,参数为 event,此时 ts 会根据左侧的事件绑定推断出右侧事件的类型。

类型兼容性

当一个类型 Y 能够赋值给另外一个类型 X 时,咱们能够认为类型 X 兼容类型 Y。

X 兼容 Y : X (目标类型) = Y (源类型)

变量兼容性

let s: string = 'abc'
s = null
复制代码

默认会提示 Type 'null' is not assignable to type 'string'. 若是将 tsconfig.json 内的 strictNullChecks 的值设置为 false,这时编译就不会报错。

能够说明 string 类型兼容 null 类型,nullstring 类型的子类型。

接口兼容性

示例以下:

interface X {
  a: any
  b: any
}

interface Y {
  a: any
  b: any
  c: any
}

let x: X = { a: 1, b: 2 }
let y: Y = { a: 1, b: 2, c: 3 }

x = y
y = x // Error: Property 'c' is missing in type 'X' but required in type 'Y'.
复制代码

y 能够赋值给 xx 不能够赋值给 y

  • 接口之间相互赋值时,成员少的会兼容成员多的。源类型必须具有目标类型的必要属性。

函数兼容性

函数个数

示例以下:

type Handler = (a: number, b: number) => void
function hof(handler: Handler) {
  return handler
}

let handler1 = (a: number) => {}
hof(handler1)

let handler2 = (a: number, b: number, c: number) => {}
hof(handler2)
// Error: Argument of type '(a: number, b: number, c: number) => void' is not assignable to parameter of type 'Handler'.

let handler3 = (a: string) => {}
hof(handler3)
// Error: Types of parameters 'a' and 'a' are incompatible. Type 'number' is not assignable to type 'string'.
复制代码

上述示例中,目标类型 handler 有两个参数,定义了三个不一样的函数进行测试。

  1. handler1 函数只有一个参数,将 handler1 传入 hof 方法做为参数(兼容)
  2. handler2 函数有三个参数,一样做为参数传入 hof 方法(不兼容)。
  3. handler2 函数参数类型与目标函数参数类型不一样(不兼容)
  • 函数参数个数,参数多的兼容参数少的。换句话说,参数多的能够被参数少的替换。

固定参数、可选参数、剩余参数

示例以下:

// 固定参数
let a = (p1: number, p2: number) => {}
// 可选参数
let b = (p1?: number, p2?: number) => {}
// 剩余参数
let c = (...args: number[]) => {}

a = b
a = c
b = a // Error
b = c // Error
c = a
c = b
复制代码
  • 固定参数兼容可选参数和剩余参数。可选参数不兼容固定参数和剩余参数,若是将 tsconfig.json 内的 strictFunctionTypes 的值设置为 false,这时编译就不会报错。剩余参数兼容固定参数和可选参数。

复杂类型

示例以下:

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

interface Point2D {
  x: number
  y: number
}
let p3d = (point: Point3D) => {}
let p2d = (point: Point2D) => {}

p3d = p2d
p2d = p3d // Error: Property 'z' is missing in type 'Point2D' but required in type 'Point3D'.
复制代码
  • 成员个数多的兼容成员个数少的,这里与接口兼容性结论相反。能够把对象拆分为参数,参数多的兼容参数少的,与函数兼容性结论一致。

若是想要上述示例中的 p2d = p3d 兼容。将 tsconfig.json 内的 strictFunctionTypes 的值设置为 false

返回值类型

示例以下:

let f = () => ({ name: 'Alice' })
let g = () => ({ name: 'Alice', location: 'Beijing' })
f = g
g = f // Error
复制代码
  • 目标函数的返回值类型,必须与源函数的返回值类型相同,或为其子类型。成员少的兼容成员多的。

函数重载

在函数部分中有介绍函数重载,这里咱们重温一下。

function overload (a: number, b: number): number function overload (a: string, b: string): string function overload (a: any, b: any): any {} 复制代码

函数重载分为两个部分,第一个部分为函数重载的列表,也就是第1、二个 overload 函数,也就是目标函数。第二个部分就是函数的具体实现,也就是第三个 overload 函数,也就是源函数。

  • 在重载列表中,目标函数的参数要大于等于源函数的参数。

枚举兼容性

示例以下:

enum Fruit { Apple, Banana }
enum Color { Red, Yellow }

let fruit: Fruit.Apple = 3
let no: number = Fruit.Apple

let color: Color.Red = Fruit.Apple // Error
复制代码
  • 枚举类型和数值(number)类型相互兼容,枚举与枚举之间相互不兼容

类兼容性

示例以下:

class A {
  constructor (p: number, q: number) {}
  id: number = 1
}

class B {
  static s = 1
  constructor (p: number) {}
  id: number = 2
}

let aa = new A(1, 2)
let bb = new B(1)

aa = bb
bb = aa
复制代码
  • 比较类与类是否兼容时,静态成员和构造函数不进行比较。成员少的兼容成员多的,父类与子类的实例相互兼容。

泛型兼容性

示例以下:

interface Empty<T> {}

let obj1: Empty<number> = {}
let obj2: Empty<String> = {}

obj1 = obj2

// 设置属性

interface Empty<T> {
  value: T
}

let obj1: Empty<number> = { value: 1 }
let obj2: Empty<String> = { value: 'a'}

obj1 = obj2 // Error
复制代码
  • 泛型接口未设置任何属性时,obj1obj2 相互兼容,若此时 Empty 设置了属性 value: T 时,obj1obj2 不兼容。

泛型函数

let log1 = <T>(x: T): T => {
  console.log('x')
  return x
}
let log2 = <U>(y: U): U => {
  console.log('y')
  return y
}

log1 = log2
复制代码
  • 泛型函数参数类型相同,参数多的兼容参数少的。

小结

  1. 结构之间兼容,成员少的兼容成员多的
  2. 函数之间兼容,参数多的兼容参数少的

类型保护

TypeScript 可以在特定的区块中保证变量属于某种肯定的类型。

能够再此区块中放心地引用此类型的属性,或者调用此类型的方法。

enum Type { Strong, Week }

class Java {
  helloJava () {
    console.log('hello java')
  }
  java: any
}

class JavaScript {
  helloJavaScript () {
    console.log('hellp javascript')
  }
  javascript: any
}

function getLanguage (type: Type, x: string | number) {
  let lang = type === Type.Strong ? new Java() : new JavaScript()
  if (lang.helloJava) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}

getLanguage(Type.Strong)
复制代码

定义 getLanuage 函数参数 type,判断 type 为强类型时,返回 Java 实例,反之返回 JavaScript 实例。

判断 lang 是否有 helloJava 方法,有则执行该方法,反之执行 JavaScript 方法。此时这里有一个错误 Property 'helloJava' does not exist on type 'Java | JavaScript'.

解决这个错误,咱们须要给 lang 添加类型断言。

if ((lang as Java).helloJava) {
    (lang as Java).helloJava()
  } else {
    (lang as JavaScript).helloJavaScript()
  }
复制代码

这显然不是很是理想的解决方案,代码可读性不好。咱们能够利用类型保护机制,以下几个方法。

instanceof

判断实例是否属于某个类

if (lang instanceof Java) {
  lang.helloJava()
} else {
  lang.helloJavaScript()
}
复制代码

in

判断一个属性是否属于某个对象

if ('java' in lang) {
  lang.helloJava()
} else {
  lang.helloJavaScript()
}
复制代码

typeof

判断一个基本类型

if (typeof x === 'string') {
  x.length
} else {
  x.toFixed(2)
}
复制代码

建立类型保护函数

function isJava(lang: Java | JavaScript): lang is Java {
  return (lang as Java).helloJava !== undefined
}

if (isJava(lang)) {
  lang.helloJava()
} else {
  lang.helloJavaScript()
}
复制代码

高级类型

介绍五种 TypeScript 高级类型:交叉类型联合类型索引类型映射类型条件类型

这些类型在前面多多少少有被提到过,咱们在统一梳理一遍。

交叉类型

& 符号,多个类型合并为一个类型,新的类型具备全部类型的特性。

interface DogInterface {
  run (): void
}
interface CatInterface {
  jump (): void
}
let pet: DogInterface & CatInterface = {
  run () {},
  jump () {}
}
复制代码

联合类型

取值能够为多种类型中的一种

let a: number | string = 1 // or '1'
复制代码

字面量联合类型

let a: 'a' | 'b' | 'c'
let b: 1 | 2 | 3
复制代码

对象联合类型

interface DogInterface {
  run (): void
}
interface CatInterface {
  jump (): void
}
class Dog implements DogInterface {
  run () {}
  eat () {}
}
class Cat implements CatInterface {
  jump () {}
  eat () {}
}
enum Master { Boy, Girl }
function getPet (master: Master) {
  let pet = master === Master.Boy ? new Dog() : new Cat()
  pet.eat()
  return pet
}
复制代码

getPet 方法体内的 pet 变量被推断为 DogCat 的联合类型。在类型未肯定的状况下,只能访问联合类型的公有成员 eat 方法。

索引类型

let obj = {
  a: 1,
  b: 2,
  c: 3
}
function getValues (obj: any, keys: string[]) {
  return keys.map(key => obj[key])
}

getValues(obj, ['a', 'b']) // [1, 2]
getValues(obj, ['d', 'e']) // [undefined, undefined]
复制代码

keys 传入非 obj 中的属性时,会返回 undefined。如何进行约束呢?这里就须要索引类型。

索引类型的查询操做符 keyof T 表示类型 T 的全部公共属性的字面量联合类型

interface Obj {
  a: number
  b: string
}
let key: keyof Obj // let key: "a" | "b"
复制代码

索引访问操做符 T[K] 对象 T 的属性 K 表明的类型

let value: Obj['a'] // let value: number
复制代码

泛型约束 T extends U

let obj = {
  a: 1,
  b: 2,
  c: 3
}
function getValues <T, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map(key => obj[key])
}

getValues(obj, ['a', 'b']) // [1, 2]
getValues(obj, ['d', 'e']) // Type 'string' is not assignable to type '"a" | "b" | "c"'.
复制代码

映射类型

能够讲一个旧的类型生成一个新的类型,好比把一个类型中的全部属性设置成只读。

interface Obj {
  a: string
  b: number
  c: boolean
}

// 接口全部属性设置成只读
type ReadonlyObj = Readonly<Obj>

// 源码
/** * Make all properties in T readonly */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 接口全部属性设置成可选
type PartialObj = Partial<Obj>

// 源码
/** * Make all properties in T optional */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// 抽取Obj子集
type PickObj = Pick<Obj, 'a' | 'b'>

// 源码
/** * From T, pick a set of properties whose keys are in the union K */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type RecordObj = Record<'x' | 'y' , Obj>
复制代码

ts 还有更多内置的映射类型,路径在 typescript/lib/lib.es5.d.ts 内提供参考。

条件类型

形式为 T extends U ? X : Y,若是类型 T 能够赋值为 U 结果就为 X 反之为 Y

type TypeName<T> =
  T extends string ? 'string' :
  T extends number ? 'number' :
  T extends boolean ? 'boolean' :
  T extends undefined ? 'undefined' :
  T extends Function ? 'function' :
  'object'

type T1 = TypeName<string> // type T1 = "string"
type T2 = TypeName<string[]> // type T2 = "object"
复制代码

(A | B) extends U ? X : Y 形式,其约等于 (A extends U ? X : Y) | (B extends U ? X : Y)

type T3 = TypeName<string | number> // type T3 = "string" | "number"
复制代码

利用该特性可实现类型过滤。

type Diff<T, U> = T extends U ? never : T

type T4 = Diff<'a' | 'b', 'a'> // type T4 = "b"

// 拆解
// Diff<'a', 'a'> | Diff<'b', 'a'>
// never | 'b'
// 'b'
复制代码

根据 Diff 再作拓展。

type NotNull<T> = Diff<T, undefined | null>

type T5 = NotNull<string | number | undefined | null> // type T5 = string | number
复制代码

以上 DiffNotNull 条件类型官方已经实现了。

Exclude<T, U> 等于 Diff<T, U>

NonNullable<T> 等于 NotNull<T>

还有更多的官方提供的条件类型,可供你们参考。

// Extract<T, U>
type T6 = Extract<'a', 'a' | 'b'> // type T6 = "a"

// ReturnType<T>
type T7 = ReturnType<() => string> // type T7 = string
复制代码
相关文章
相关标签/搜索