typescript 基础知识

151 阅读16分钟

什么是TypeScript

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于的面向对象编程。 TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系: 0.png

TypeScript 与 JavaScript 的区别

TypeScriptJavaScript
JavaScript 的超集用于解决项目的代码复杂性一种脚本语言,用于创建动态网页
可以在编译期间发现并纠正错误作为一种解释型语言,只能在运行时发现错误
强类型,支持静态和动态类型弱类型,没有静态类型选项
最终被编译成 JavaScript 代码,使浏览器可以理解可以直接在浏览器中使用
支持模块、泛型和接口不支持模块,泛型或接口
社区的支持仍在增长,而且还不是很大大量的社区支持以及大量文档和解决问题的支持

TypeScript 的使用

  • 安装TypeScript win + R 输入 cmd 打开控制台
$ npm install -g typescript

如果有报错,直接使用管理员身份打开控制台

  • 验证TypeScript
$ tsc -V

当然,对刚入门 TypeScript 的小伙伴来说,也可以不用安装 typescript,而是直接使用线上的 线上尝鲜 来学习新的语法或新特性。通过配置 TS Config 的 Target,可以设置不同的编译目标,从而编译生成不同的目标代码。 如图: TypeScript编译成ES5

1.png

典型 TypeScript 工作流程

2.png 在上图中包含 3 个 ts 文件:a.tsb.tsc.ts。这些文件将被 TypeScript 编译器,根据配置的编译选项编译成 3 个 js 文件,即 a.jsb.jsc.js。对于大多数使用 TypeScript 开发的 Web 项目,我们还会对编译生成的 js 文件进行打包处理,然后在进行部署。

TypeScript 基础类型

Boolean 类型

let flag: boolean = false
let flag: boolean

Number 类型

let num: number = 10
let num: number

String 类型

let name: string = '江南小魏晨'
let name: string

Symbol 类型

const sym = Symbol()
let obj = {
  [sym]: '江南小魏晨',
}

Array 类型

let list: number[] = [1, 2, 3]
 // Array<number>泛型语法
let listB: Array<number> = [1, 2, 3]

Enum 类型

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript 支持数字的和基于字符串的枚举。

  • 数字枚举
/** 默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。*/ 
enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}
let dir: Direction = Direction.NORTH
// ES5 代码
var Direction;
(function (Direction) {
  Direction[(Direction['NORTH'] = 0)] = 'NORTH'
  Direction[(Direction['SOUTH'] = 1)] = 'SOUTH'
  Direction[(Direction['EAST'] = 2)] = 'EAST'
  Direction[(Direction['WEST'] = 3)] = 'WEST'
})(Direction || (Direction = {}))
var dir = Direction.NORTH
// 当然,直接设置值也是可以的
enum Direction {
  NORTH = 3,
  SOUTH,
  EAST,
  WEST,
}
  • 字符串枚举 在 TypeScript 2.4 + 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Direction {
  NORTH = 'NORTH',
  SOUTH = 'SOUTH',
  EAST = 'EAST',
  WEST = 'WEST',
}
// ES5 代码
var Direction
(function (Direction) {
    Direction['NORTH'] = 'NORTH'
    Direction['SOUTH'] = 'SOUTH'
    Direction['EAST'] = 'EAST'
    Direction['WEST'] = 'WEST'
})(Direction || (Direction = {}))

// 通过观察数字枚举和字符串枚举的编译结果,我们可以知道数字枚举除了支持 从成员名称到成员值 的普通映射之外,它还支持 从成员值到成员名称 的反向映射:
enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dirName = Direction[0] // NORTH
let dirVal = Direction['NORTH'] // 0
  • 常量枚举 除了数字枚举和字符串枚举之外,还有一种特殊的枚举 —— 常量枚举。它是使用 const 关键字修饰的枚举,常量枚举会使用内联语法,不会为枚举类型编译生成任何 JavaScript。
const enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}
let dir: Direction = Direction.NORTH
// ES5 代码
var dir = 0 /* NORTH */
  • 异构枚举 异构枚举的成员值是数字和字符串的混合:
enum Enum {
  A,
  B,
  C = 'C',
  D = 'D',
  E = 8,
  F,
}
// ES5 代码
var Enum
(function (Enum) {
    Enum[Enum['A'] = 0] = 'A'
    Enum[Enum['B'] = 1] = 'B'
    Enum['C'] = 'C'
    Enum['D'] = 'D'
    Enum[Enum['E'] = 8] = 'E'
    Enum[Enum['F'] = 9] = 'F'
})(Enum || (Enum = {}))
// 输出查看
console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A

Any 类型

在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。

let notSure: any = 666
notSure = 'semlinker'
notSure = false

any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查

let value: any

value.foo.bar // OK
value.trim() // OK
value() // OK
new value() // OK
value[0][1] // OK

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

Unknown 类型

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。

let value: unknown

value = true // OK
value = 42 // OK
value = 'Hello World'; // OK
value = [] // OK
value = {} // OK
value = Math.random() // OK
value = null // OK
value = undefined // OK
value = new TypeError() // OK
value = Symbol('type') // OK

value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

let value: unknown

let value1: unknown = value // OK
let value2: any = value // OK
let value3: boolean = value // Error
let value4: number = value // Error
let value5: string = value // Error
let value6: object = value // Error
let value7: any[] = value // Error
let value8: Function = value // Error

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。

如果对 unknown 的值执行操作时会发生什么?

let value: unknown

value.foo.bar // Error
value.trim() // Error
value() // Error
new value() // Error
value[0][1] // Error

将 value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。

Tuple 类型

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

let tupleType: [string, boolean]
tupleType = ['江南小魏晨', true]
console.log(tupleType[0]) // semlinker
console.log(tupleType[1]) // true
// 在元组初始化的时候,如果出现类型不匹配的话,比如
tupleType = [true, '江南小魏晨']
// TypeScript 编译器会提示以下错误信息
[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.
// 很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误
tupleType = ['江南小魏晨']
Property '1' is missing in type '[string]' but required in type '[string, boolean]'

Void 类型

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void

// 声明函数返回值为void
function warnUser(): void {
  console.log('我叫江南小魏晨')
}
// ES5 代码
'use strict'
function warnUser() {
  console.log('我叫江南小魏晨')
}

需要注意的是,声明一个 void 类型的变量没有什么作用,因为在严格模式下,它的值只能为 undefined

let unusable: void = undefined;

Null 和 Undefined 类型

TypeScript 里,undefinednull 两者有各自的类型分别为 undefinednull

let u: undefined = undefined;
let n: null = null;

object, Object 和 {} 类型

  • object 类型 object 类型是:TypeScript 2.2 引入的新类型,它用于表示非原始类型。

interface ObjectConstructor {
  create(o: object | null): any;
  // ...
}

const proto = {}

Object.create(proto)     // OK
Object.create(null)      // OK
Object.create(undefined) // Error
Object.create(1337)      // Error
Object.create(true)      // Error
Object.create('oops')    // Error
  • Object类型
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}

ObjectConstructor 接口定义了 Object 类的属性

// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;
  readonly prototype: Object;
  getPrototypeOf(o: any): any;
  // ···
}

declare var Object: ObjectConstructor;

Object 类的所有实例都继承了 Object 接口中的所有属性

  • {} 类型 {} 类型描述了一个没有成员的对象。当你试图访问这样一个对象的任意属性时,TypeScript 会产生一个编译时错误。
// Type {}
const obj = {};

// Error: Property 'prop' does not exist on type '{}'.
obj.prop = '江南小魏晨'

但是,你仍然可以使用在 Object 类型上定义的所有属性和方法,这些属性和方法可通过 JavaScript 的原型链隐式地使用

// Type {}
const obj = {}

// '[object Object]'
obj.toString()

Never 类型

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message)
}

function infiniteLoop(): never {
  while (true) {}
}

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === 'string') {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === 'number') {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

else 分支里面,我们把收窄为 neverfoo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的猪队友修改了 Foo 的类型

type Foo = string | number | boolean

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保 controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码

TypeScript 断言

类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。 通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用

  • “尖括号” 语法
let someValue: any = '这是一段字符串'
let strLength: number = (<string>someValue).length
  • as 语法
let someValue: any = '这是一段字符串'
let strLength: number = (someValue as string).length

非空断言

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 nullundefined

  • 忽略 undefinednull 类型
function myFunc(maybeString: string | undefined | null) {
  // Type 'string | null | undefined' is not assignable to type 'string'.
  // Type 'undefined' is not assignable to type 'string'. 
  const onlyString: string = maybeString // Error
  const ignoreUndefinedAndNull: string = maybeString! // Ok
}
  • 调用函数时忽略 undefined 类型
type NumGenerator = () => number

function myFunc(numGenerator: NumGenerator | undefined) {
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator() // Error
  const num2 = numGenerator!() //OK
}

因为 ! 非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中,要特别注意。

const a: number | undefined = undefined
const b: number = a!
console.log(b)
// ES5 代码
"use strict";
const a = undefined
const b = a
console.log(b)

虽然在 TS 代码中,我们使用了非空断言,使得 const b: number = a!; 语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,! 非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined

  • 确定赋值断言 在 TypeScript 2.7 版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值。
let x: number
initialize()
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x) // Error

function initialize() {
  x = 10
}
// 很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言
let x!: number
initialize()
console.log(2 * x) // Ok

function initialize() {
  x = 10
}

通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。

类型守卫

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。 换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护

in 关键字

interface Admin {
  name: string
  privileges: string[]
}

interface Employee {
  name: string
  startDate: Date
}

type UnknownEmployee = Employee | Admin

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log('Name: ' + emp.name)
  if ('privileges' in emp) {
    console.log('Privileges: ' + emp.privileges)
  }
  if ('startDate' in emp) {
    console.log('Start Date: ' + emp.startDate)
  }
}

typeof 关键字

function padLeft(value: string, padding: string | number) {
  if (typeof padding === 'number') {
      return Array(padding + 1).join(' ') + value
  }
  if (typeof padding === 'string') {
      return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}

typeof 类型保护只支持两种形式:typeof v === 'typename'typeof v !== 'typename''typename' 必须是 'number''string''boolean''symbol'。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

instanceof 关键字

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(' ')
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value
  }
}

let padder: Padder = new SpaceRepeatingPadder(6)

if (padder instanceof SpaceRepeatingPadder) {
  // padder的类型收窄为 'SpaceRepeatingPadder'
}

自定义类型保护的类型谓词

function isNumber(x: any): x is number {
  return typeof x === 'number'
}

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

联合类型和类型别名

联合类型

联合类型通常与 nullundefined 一起使用

const sayHello = (name: string | undefined) => {
  /* ... */
}
// 这里 name 的类型是 string | undefined 意味着可以将 string 或 undefined 的值传递给sayHello 函数
sayHello('我叫江南小魏晨')
sayHello(undefined)

凭直觉就能知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说 还有另一种用法

let num: 1 | 2 = 1
type EventNames = 'click' | 'mousedown' | 'mousemove'
// 1、2 或 'click' 被称为字面量类型,用来约束取值只能是某几个值中的一个

可辨识联合

TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫 这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块

  • 可辨识 可辨识要求联合类型中的每个元素都含有一个单例类型属性
enum CarTransmission {
  Automatic = 200,
  Manual = 300
}

interface Motorcycle {
  vType: 'motorcycle' // discriminant
  make: number // year
}

interface Car {
  vType: 'car' // discriminant
  transmission: CarTransmission
}

interface Truck {
  vType: 'truck' // discriminant
  capacity: number // in tons
}

在上述代码中,我们分别定义了 Motorcycle、 CarTruck 三个接口,在这些接口中都包含一个 vType 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关

  • 联合类型
type Vehicle = Motorcycle | Car | Truck

现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的车

  • 类型守卫 下面我们来定义一个 evaluatePrice 方法,该方法用于根据车辆的类型、容量和评估因子来计算价格
const EVALUATION_FACTOR = Math.PI

function evaluatePrice(vehicle: Vehicle) {
  return vehicle.capacity * EVALUATION_FACTOR
}

const myTruck: Truck = { vType: "truck", capacity: 9.5 }
evaluatePrice(myTruck)
// 编译会报错!!!!!!!!!
Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.

Motorcycle 接口中,并不存在 capacity 属性,而对于 Car 接口来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice 方法

function evaluatePrice(vehicle: Vehicle) {
  switch(vehicle.vType) {
    case 'car'
      return vehicle.transmission * EVALUATION_FACTOR
    case 'truck':
      return vehicle.capacity * EVALUATION_FACTOR
    case 'motorcycle':
      return vehicle.make * EVALUATION_FACTOR
  }
}

我们使用 switchcase 运算符来实现类型守卫,从而确保在 evaluatePrice 方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该车辆类型所对应的价格

类型别名

类型别名用来给一个类型起个新名字

type Message = string | string[]

let greet = (message: Message) => {
  // ...
}

交叉类型

在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

type PartialPointX = { x: number }
type Point = PartialPointX & { y: number }

let point: Point = {
  x: 1,
  y: 1
}

先定义了 PartialPointX 类型,接着使用 & 运算符创建一个新的 Point 类型,表示一个含有 xy 坐标的点,然后定义了一个 Point 类型的变量并初始化

同名基础类型属性的合并

interface X {
  c: string
  d: string
}

interface Y {
  c: number
  e: string
}

type XY = X & Y
type YX = Y & X

let p: XY
let q: YX

接口 X 和接口 Y 都含有一个相同的成员 c,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型是不是可以是 stringnumber 类型呢

3.png

4.png 为什么接口 X 和接口 Y 混入后,成员 c 的类型会变成 never 呢?这是因为混入后成员 c 的类型为 string & number,即成员 c 的类型既可以是 string 类型又可以是 number 类型。很明显这种类型是不存在的,所以混入后成员 c 的类型为 never

同名非基础类型属性的合并

在上面示例中,刚好接口 X 和接口 Y 中内部成员 c 的类型都是基本数据类型,那么如果是非基本数据类型的话,又会是什么情形

interface D { d: boolean }
interface E { e: string }
interface F { f: number }

interface A { x: D }
interface B { x: E }
interface C { x: F }

type ABC = A & B & C

let abc: ABC = {
  x: {
    d: true,
    e: 'semlinker',
    f: 666
  }
}
console.log('abc:', abc)

运行!!!控制台会输出以下结果

5.png

在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并

TypeScript 函数

TypeScriptJavaScript
含有类型无类型
箭头函数箭头函数(ES2015)
函数类型无函数类型
必填和可选参数所有参数都是可选的
默认参数默认参数
剩余参数剩余参数
函数重载无函数重载

箭头函数

  • 常见语法
myBooks.forEach(() => console.log('reading'))

myBooks.forEach(title => console.log(title))

myBooks.forEach((title, idx, arr) =>
  console.log(idx + '-' + title)
);

myBooks.forEach((title, idx, arr) => {
  console.log(idx + '-' + title)
})
  • 使用示例
// 未使用箭头函数
function Book() {
  let self = this
  self.publishDate = 2016
  setInterval(function () {
    console.log(self.publishDate)
  }, 1000)
}

// 使用箭头函数
function Book() {
  this.publishDate = 2016
  setInterval(() => {
    console.log(this.publishDate)
  }, 1000)
}

参数类型和返回类型

function createUserId(name: string, id: number): string {
  return name + id
}

函数类型

let IdGenerator: (chars: string, nums: number) => string

function createUserId(name: string, id: number): string {
  return name + id
}

可选参数及默认参数

// 可选参数
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

// 默认参数
function createUserId(name: string = 'semlinker',id: number,age?: number): string {
  return name + id
}

在声明函数时,可以通过 ? 号来定义可选参数,比如 age?: number 这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误

剩余参数

function push(array, ...items) {
  items.forEach(item => {
    array.push(item)
  })
}
let a = []
push(a, 1, 2, 3)

函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力 Java的方法重载,是不是类似?!

function add(a: number, b: number): number
function add(a: string, b: string): string
function add(a: string, b: number): string
function add(a: number, b: string): string
function add(a: Combinable, b: Combinable) {
  // type Combinable = string | number
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString()
  }
  return a + b
}

在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法 方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同

class Calculator {
  add(a: number, b: number): number
  add(a: string, b: string): string
  add(a: string, b: number): string
  add(a: number, b: string): string
  add(a: Combinable, b: Combinable) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString()
  }
    return a + b
  }
}

const calculator = new Calculator()
const result = calculator.add('Semlinker', ' Kakuqo')

当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法

TypeScript 数组

数组解构

let x: number, y: number, z: number
let five_array = [0,1,2,3,4]
[x,y,z] = five_array

数组展开运算符

let two_array = [0, 1]
let five_array = [...two_array, 2, 3, 4]

数组遍历

let colors: string[] = ['red', 'green', 'blue']
for (let i of colors) {
  console.log(i)
}

TypeScript 对象

对象解构

let person = {
  name: '江南小魏晨',
  gender: '男',
};

let { name, gender } = person

对象展开运算符

let person = {
  name: '江南小魏晨',
  gender: '男',
  address: '文鼎广场'
}

// 组装对象
let personWithAge = { ...person, age: 33 }

// 获取除了某些项外的其它项
let { name, ...rest } = person

TypeScript 接口

在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。 TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述

对象的形状

interface Person {
  name: string
  age: number
}

let semlinker: Person = {
  name: '江南小魏晨',
  age:18,
}

可选 | 只读属性

interface Person {
  readonly name: string
  age?: number
}

只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改

let a: number[] = [1, 2, 3, 4]
let ro: ReadonlyArray<number> = a
ro[0] = 12 // error!
ro.push(5) // error!
ro.length = 100 // error!
a = ro // error!

任意属性

有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名

interface Person {
  name: string
  age?: number
  // 关注点在这
  [propName: string]: any
}

const p1 = { name: "semlinker" }
const p2 = { name: "lolo", age: 5 }
const p3 = { name: "kakuqo", sex: 1 }

接口与类型别名的区别

Objects/Functions

接口和类型别名都可以用来描述对象的形状或函数签名

  • 接口
interface Point {
  x: number
  y: number
}

interface SetPoint {
  (x: number, y: number): void
}
  • 类型别名
type Point = {
  x: number
  y: number
}

type SetPoint = (x: number, y: number) => void

Other Types

与接口类型不一样,类型别名可以用于一些其他类型,比如原始类型、联合类型和元组

// 原始类型
type Name = string

// 对象
type PartialPointX = { x: number; }
type PartialPointY = { y: number; }

// 联合类型
type PartialPoint = PartialPointX | PartialPointY

// 元组
type Data = [number, string]

Extend 继承

接口和类型别名都能够被扩展,但语法有所不同。此外,接口和类型别名不是互斥的。接口可以扩展类型别名,而反过来是不行的

  • 接口继承接口
interface PartialPointX { x: number }
interface Point extends PartialPointX { 
  y: number
}
  • 类型继承类型
type PartialPointX = { x: number }
type Point = PartialPointX & { y: number }
  • 类型继承接口
interface PartialPointX { x: number }
type Point = PartialPointX & { y: number }

Implements 实现类

类可以以相同的方式实现接口或类型别名,但类不能实现使用类型别名定义的联合类 Java能不能用方法实现类型?!

interface Point {
  x: number
  y: number
}

class SomePoint implements Point {
  x = 1
  y = 2
}

type Point2 = {
  x: number
  y: number
}

class SomePoint2 implements Point2 {
  x = 1
  y = 2
}

type PartialPoint = { x: number } | { y: number }

// A class can only implement an object type or 
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
  x = 1
  y = 2
}

Declaration merging 合并接口

与类型别名不同,接口可以定义多次,会被自动合并为单个接口

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

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

TypeScript 类

类的属性与方法

在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法 在 TypeScript 中,我们可以通过 Class 关键字来定义一个类

class Greeter {
  // 静态属性
  static cname: string = 'Greeter'
  // 成员属性
  greeting: string

  // 构造函数 - 执行初始化操作
  constructor(message: string) {
    this.greeting = message
  }

  // 静态方法
  static getClassName() {
    return 'Class name is Greeter'
  }

  // 成员方法
  greet() {
    return 'Hello, ' + this.greeting
  }
}

let greeter = new Greeter('world')

成员属性与静态属性,成员方法与静态方法有什么区别呢?直接上 ES5 代码

"use strict";
var Greeter = /** @class */ (function () {
    // 构造函数 - 执行初始化操作
    function Greeter(message) {
        this.greeting = message
    }
    // 静态方法
    Greeter.getClassName = function () {
        return 'Class name is Greeter'
    };
    // 成员方法
    Greeter.prototype.greet = function () {
        return 'Hello, ' + this.greeting
    };
    // 静态属性
    Greeter.cname = 'Greeter'
    return Greeter;
}());
var greeter = new Greeter('world')

ECMAScript 私有字段

TypeScript 3.8 版本就开始支持ECMAScript 私有字段

class Person {
  #name: string

  constructor(name: string) {
    this.#name = name
  }

  greet() {
    console.log(`Hello, my name is ${this.#name}!`)
  }
}

let semlinker = new Person('snake')
semlinker.#name

与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:

  • 私有字段以 # 字符开头,有时我们称之为私有名称
  • 每个私有字段名称都唯一地限定于其包含的类
  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 publicprivate
  • 私有字段不能在包含的类之外访问,甚至不能被检测到

访问器

在 TypeScript 中,我们可以通过 gettersetter 方法来实现数据的封装和有效性校验,防止出现异常数据 webstrom ideal 直接一键生成get set方法

let passcode = 'Hello TypeScript'

class Employee {
  private _fullName: string

  get fullName(): string {
    return this._fullName
  }

  set fullName(newName: string) {
    if (passcode && passcode == 'Hello TypeScript') {
      this._fullName = newName
    } else {
      console.log('Error: Unauthorized update of employee!')
    }
  }
}

let employee = new Employee()
employee.fullName = 'snake'
if (employee.fullName) {
  console.log(employee.fullName)
}

类的继承

继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系 在 TypeScript 中,我们可以通过 extends 关键字来实现继承

class Animal {
  name: string
  
  constructor(theName: string) {
    this.name = theName
  }
  
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`)
  }
}

class Snake extends Animal {
  constructor(name: string) {
    super(name) // 调用父类的构造函数
  }
  
  move(distanceInMeters = 5) {
    console.log('move方法正在进行中.....')
    super.move(distanceInMeters)
  }
}

let sam = new Snake('张益达')
sam.move()

抽象类

使用 abstract 关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法

abstract class Person {
  constructor(public name: string){}

  abstract say(words: string) :void
}

// 抽象类不能直接实例化
const lolo = new Person() // Error

抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类

abstract class Person {
  constructor(public name: string){}

  // 抽象方法
  abstract say(words: string) :void
}

class Developer extends Person {
  constructor(name: string) {
    super(name)
  }
  
  say(words: string): void {
    console.log(`${this.name} says ${words}`)
  }
}

const yy = new Developer('江南小魏晨')
yy.say('I love ts!')

类方法重载

前面已经说了函数重载。对于类的方法来说,它也支持重载

class ProductService {
    getProducts(): void
    getProducts(id: number): void
    getProducts(id?: number) {
      if(typeof id === 'number') {
          console.log(`获取id为 ${id} 的产品信息`)
      } else {
          console.log(`获取所有的产品信息`)
      }  
    }
}

const productService = new ProductService()
productService.getProducts(666) // 获取id为 666 的产品信息
productService.getProducts() // 获取所有的产品信息 

TypeScript 泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能 在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件 设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值 泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类

泛型语法

跟Java中一样,<T>其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型

6.png 参考上面的图片,当我们调用 identity<number>(1)number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T> 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 number 类型 其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,还有

  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型 其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数
function identity <T, U>(value: T, message: U) : T {
  console.log(message)
  return value
}

console.log(identity<Number, string>(18, '江南小魏晨'))

7.png 除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号

function identity <T, U>(value: T, message: U) : T {
  console.log(message)
  return value
}
// 两个区别在这
console.log(identity(18, '江南小魏晨'))

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们

泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T
}

泛型类

class GenericNumber<T> {
  zeroValue: T
  add: (x: T, y: T) => T
}

let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function (x, y) {
  return x + y
}

TypeScript 装饰器

装饰器是什么

  • 它是一个表达式
  • 该表达式被执行后,返回一个函数
  • 函数的入参分别为 target、name 和 descriptor
  • 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象

装饰器的分类

  • 类装饰器(Class decorators)
  • 属性装饰器(Property decorators)
  • 方法装饰器(Method decorators)
  • 参数装饰器(Parameter decorators) 需要注意的是,若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项

命令行:

$ tsc --target ES5 --experimentalDecorators

tsconfig.json

{
  "compilerOptions": {
     "target": "ES5",
     "experimentalDecorators": true
   }
}

编译上下文

tsconfig.json 的作用

  • 用于标识 TypeScript 项目的根路径
  • 用于配置 TypeScript 编译器
  • 用于指定编译的文件

tsconfig.json 重要字段

  • files - 设置要编译的文件的名称
  • include - 设置需要进行编译的文件,支持路径模式匹配
  • exclude - 设置无需进行编译的文件,支持路径模式匹配
  • compilerOptions - 设置与编译流程相关的选项

compilerOptions 选项

compilerOptions 支持很多选项,常见的有 baseUrl、 targetbaseUrlmoduleResolutionlib

{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}
~~