TypeScript 进阶语法

本文接 TypeScript 基础语法入门

进阶语法


联合类型

什么是联合类型呢,其实我们在基础语法中就遇到过,通过 '|' 连接两个或多个类型,表示这个属性可以是多个类型的数据。

举个栗子:假设我们有一个鸟的类型和一个狗的类型,这两个类型上都有一个是否会飞的属性,然后鸟还有一个唱歌的方法,而狗则是有一个狗叫的方法,我们在一个函数中希望传入的类型是鸟和狗的联合类型。

我们用代码复现这个栗子:

interface Bird {
  fly: boolean;
  sing: () => {};
}
interface Dog {
  fly: boolean;
  bark: () => {};
}
function trainAnial(animal: Bird | Dog) { // 定义一个联合类型
  animal. // 编辑器会自动提示 fly 属性,因为 fly 是两个类型共有的
}
类型断言

我们还是用上面那个栗子,我们想在函数中直接调用 sing() ,但是很明显这样会报错,因为 sing() 不是两个类型的公共方法,ts 不知道你传入的类型到底会是 Bird 还是 Dog ,但是我们很明确的知道我们一定会传入一个 Bird 类型,它一定会有一个 sing() 方法,所以我们此时就可以用 as 关键字将传入的 animal 断言成 Bird 类型。代码如下:

function trainAnial(animal: Bird | Dog) {
  // 加入入参的类型上 fly 属性为 true,则它是一个小鸟属性
  if (animal.fly) {
    // 直接使用 as 将其断言成 Bird 类型并调用 sing()
    (animal as Bird).sing()
  } else {
    // 反之将其断言成一个 Dog 类型并调用 brak() 
    (animal as Dog).bark()
  }
}
类型保护

我们上面用了类型断言的方式去实现类型保护,其实我们还可以用很多别的方式来实现类型保护:

使用 in 语法实现类型保护
function trainAnial(animal: Bird | Dog) {
  if ('sing' in animal) { // 如果 animal 上面有 sing 方法那么 ts 就会推断出它是 Bird 类型
    animal.sing()
  } else { // 反之 ts 就会推断出它是 Dog 类型
    animal.bark()
  }
}

当然上面 else 能够直接推断是因为我们只有两个类型进行联合,除了 in 语法我们还可以使用 typeof 进行类型保护。

使用 typeof 实现类型保护

这里我们换个更简单的栗子来说明:假如一个函数有两个值,这两个值都是 number | string 类型,我们希望如果其中一个值时字符串时返回两个值拼接,如果两个值都是 number 则返回它们进行求和。

不适用类型保护直接求和:

function add(first: number | string, second: number | string) {
  return first + second // error 运算符 + 不能用于两个 string | number
}

使用 typeof 进行类型保护:

function add(first: number | string, second: number | string) {
  if (typeof first === 'string' || typeof second === 'string') {
    return `${first}${second}`
  }
  return first + second
}
使用 instanceof 进行类型保护

改造成功,感觉是不是和 js 中的类型判断很像,在 js 中我们知道我们可以使用 typeof 判断基础类型,使用 instanceof 进行引用类型的判断。那么我们也是使用两个引用类型,看 instanceof 能否帮我们实现类型保护:

class NumberObj {
  constructor(public count: number) { }
}
function add(first: Object | NumberObj, second: Object | NumberObj) {
  if (first instanceof NumberObj && second instanceof NumberObj) {
    return first.count + second.count
  }
  return 0
}

我们使用的是 clss 类来定义类型,为什么不使用 interface 呢?因为 interface 无法使用 instanceof 来进行类型保护。当然类型保护的方法还有很多,大家可以在官网自行查阅。

枚举类型

枚举也是项目开发中经常会遇到的类型,在了解枚举之前,我们先来看一个我们日常在写 js 中经常会写的一些代码:

const Status = {
  OFFLINE: 0,
  ONLINE: 1,
  DELETED: 2
}
function getResult(status: number) {
  if (status === Status.OFFLINE) {
    return 'offline'
  } else if (status === Status.ONLINE) {
    return 'online'
  } else if (status === Status.DELETED) {
    return 'deleted'
  }
}
console.log(getResult(Status.DELETED))

将常量定义为指定变量,通过返回值确认和常量是否匹配输出对应内容。现在我们使用枚举将上述代码改造一下:

enum Status {
  OFFLINE,
  ONLINE,
  DELETED
}
function getResult(status: number) {
  if (status === Status.OFFLINE) {
    return 'offline'
  } else if (status === Status.ONLINE) {
    return 'online'
  } else if (status === Status.DELETED) {
    return 'deleted'
  }
}
console.log(getResult(Status.DELETED))

细心的你应该发现,我们只是在常量的定义处将 const 改成了 enum 并且没有给它赋值,但是却实现了相同的功能。我们先来尝试着总结一下枚举的基本特性:

我们定义一个数字枚举,如果我们不初始化第一个属性的值,那么它将从 0 开始然后依次递增,如果我们初始化第一个属性的值为 3 ,那么后面属性的值就会从 3 开始递增,而如果我们不初始化第一个值,而是初始化第三个值为 5,那么第一个值和第二个值就是 01,从第三个值开始就是从 5 开始递增。

栗子说话:

enum Direction {
  Up = 1,
  Down,
  Left,
  Right
}
console.log(Direction.Up) // 1
console.log(Direction.Down) // 2
console.log(Direction.Left) // 3
console.log(Direction.Right) // 4

enum Direction {
  Up,
  Down,
  Left = 5,
  Right
}
console.log(Direction.Up) // 0
console.log(Direction.Down) // 1
console.log(Direction.Left) // 5
console.log(Direction.Right) // 6

当然,我们还可以使用枚举做反向映射,啥是反向映射?看个栗子就明白了:

enum Direction {
  Up,
  Down,
  Left = 5,
  Right
}
console.log(Direction[0]) // Up
console.log(Direction[1]) // Down
console.log(Direction[5]) // Left
console.log(Direction[6]) // Right

前面我们都是在说数字枚举,还有我们也可以做字符串枚举:

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}
console.log(Direction.Up) // UP
const value = 'UP'
if (value === Direction.Up) {
  console.log('go up!')
}
泛型

ts 中用的最多的就是泛型与接口,当然这也是 ts 中比较难的地方,掌握泛型基本就可以在项目中比较好的使用 ts 了。我们先来看一下官网关于泛型的定义:

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

泛型与函数

光看概念我们可能有点迷糊,我们先来看一个简单的栗子:

function join(first: number | string, second: number | string) {
  return `${first}${second}`
}
let result = join(1, '1')
console.log(result)

虽然我们规定了 firstsecond 可以为 numberstring 类型,但是如果我们想在调用函数的时候如果传入的第一个值为 string 类型,那么传入的第二个值也必须为 string 类型,此时我们用前面学习的知识解决不了这个问题,但是结合泛型的概念,我们可以尝试使用泛型来解决:

// 泛型就是泛指的类型,此处我们定义泛型<ABC>,当然我们可以随便命名
// 然后将入参定义为 ABC
function join<ABC>(first: ABC, second: ABC) {
  return `${first}${second}`
}
// 我们在函数调用的位置希望传入的实参都是 string 类型
let result = join<string>(1, '1') // error 类型 number 的参数不能赋值给 string 类型
let result = join<string>('1', '1') // ok

使用泛型我们可以实现在函数调用的时候在指定类型,而函数声明的时候都用我们自定义的泛型名代指它的类型。当然泛型的命名官网建议我们自定义的名字最好用 T

function echo<T>(arg: T): T {
  return arg
}
const result = echo(123) // result 是 number 类型
const result1 = echo('str') // result1 是 string 类型

虽然我们使用泛型在调用函数时统一规定了它为一种类型,但是如果我们也希望使用泛型的时候能同时传入 stringnumber 类型,那么我们应该如何实现呢?此时我们可以在泛型中同时传入多个类型。

function join<T, P>(first: T, second: P) {
  return `${first}${second}`
}
// 此时我们就限定了传入的类型为 number 和 string
let result = join<number, string>(1, '1')
泛型与类
class DataManager {
  constructor(public data: string[] | number[]) { }
  getItem(index: number): string | number {
    return this.data[index]
  }
}
const data = new DataManager(['1'])
data.getItem(0)

上述代码中我们定义了一个类,在构造器中初始化了一个 data 它可能为字符串数组或者为数字数组,但是如果我们后面希望他里面可能有 nullundefined 等其他类型,那么我们就要写很多个联合类型在这里定义。那么我们能否使用泛型将类型的定义写的更灵活呢?

class DataManager<T> {
  constructor(public data: T[]) { }
  getItem(index: number): T {
    return this.data[index]
  }
}
const data = new DataManager(['1', 2, null, undefined])
let result = data.getItem(0)

我们将类定义成了一个泛型,那么我们在数组中传入各种类型的值,ts 就会自动帮我们做类型推断,将这个类推断出我们在数组中传入值的联合类型。

泛型约束

先来看一个问题,虽然我们使用泛型可以完美解决传入的类型和返回的类型相同,但是如果遇到如下栗子:

function echo<T>(arg: T): T {
  console.log(arg.length)
  return arg
}

上例中我们不确定我们传入的泛型 T 是否有 length 属性,我们可以给他传入任意类型,当然有些不包括 length 属性,那样就会报错。

泛型约束的概念:在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法。为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束。

interface IWithLength {
  length: number
}
function echo<T extends IWithLength>(arg: T): T {
  console.log(arg.length)
  return arg
}
const result = echo(123) // error 类型 number 的参数因为没有 length 属性就会被检测出来错误
const result = echo('123') // ok
const result1 = echo([1, 2, 3]) // ok

那么此时我们用泛型约束来结合上面泛型与类的栗子,我们希望 getItem() 返回的是每一个 itemname 属性,栗子如下:

class DataManager<T> {
  constructor(public data: T[]) { }
  getItem(index: number): T {
    return this.data[index].name // 报错,因为 ts 不确定传入的泛型是否会有 name 属性
  }
}

此时我们就结合刚刚说的泛型约束,约束这个泛型必须有 name 属性:

interface Item {
  name: string
}
// 让泛型继承我们定义的接口类型,那么就强制要求传入的实参中必须要有 name 属性
class DataManager<T extends Item> {
  constructor(public data: T[]) { }
  getItem(index: number): string {
    return this.data[index].name
  }
}
const data = new DataManager([{
  name: 'cc'
}])
let result = data.getItem(0)

当然在类中我们不仅可以使泛型继承接口,还可以只是继承一些联合类型。如下栗子:

class DataManager<T extends number | string> {
  constructor(public data: T[]) { }
  getItem(index: number): T {
    return this.data[index]
  }
}
const data = new DataManager<null>([]) // error
const data = new DataManager<string>([]) // ok
let result = data.getItem(0)

当然我们还可以使用泛型作为一个具体的类型注解,我们知道类型注解就是在定义的变量后面直接跟上这个变量的类型来标识它,那我们如何来定义一个函数传入泛型的类型注解呢:

const func: <T>() => string = <T>() => {
  return '123'
}

上述代码中我们定义了一个函数类型,它需要传入一个泛型,然后返回一个 string 类型,我们在构建这个函数的时候也要指定这个函数需要传入一个泛型,当然上述代码我们没有传入形参,所以泛型可能没有定义到形参上,我们可以改一下上面的代码:

function hello<T>(params: T) {
  return params
}
const func: <T>(params: T) => T = hello
泛型与接口
interface KeyPair<T, U> {
  key: T;
  value: U;
}
let kp1: KeyPair<number, string> = { key: 1, value: "str"}
let kp2: KeyPair<string, number> = { key: "str", value: 123}
泛型中 keyof 语法的使用

很多时候我们会遇到下面这种情况,先上代码:

interface Person {
  name: string;
  age: number;
  gender: string;
}
class Teacher {
  constructor(public info: Person) { }
  getInfo(key: string) {
    return this.info[key] // 编辑器报错提醒
  }
}
const teacher = new Teacher({
  name: 'cc',
  age: 18,
  gender: 'male'
})
let result = teacher.getInfo('name')
console.log(result)

聪明的你一定知道报错原因,因为我们传入的可能不是我们接口中约束的三个字段,我们在使用中很可能传入 teacher.getInfo('hello') 这种没有在接口中约定过的 hello 字段,所以 ts 会给我们报错提醒,此时如果我们使用类型保护来纠正一下代码:

class Teacher {
  constructor(public info: Person) { }
  getInfo(key: string) {
    // 约束 key 的值只能为这三个才返回
    if (key === 'name' || key === 'age' || key === 'gender') {
      return this.info[key]
    }
  }
}
let result = teacher.getInfo('name') // 但是此时 result 确实 string | number | undefined

虽然看似不报错了,但是此时我们仍然传 hello 仍然可以得到返回值 undefined ,我们肯定希望只要我们传入的不是 nameagegender 就提示错误信息,此时我们就可以使用泛型中的 keyof 语法:

// keyof Person 循环读取 Person 的 key
// T extends 'name' type T = 'name'
// T extends 'age' type T = 'age'
// T extends 'gender' type T = 'gender'
class Teacher {
  constructor(public info: Person) { }
  getInfo<T extends keyof Person>(key: T): Person[T] {
    return this.info[key]
  }
}

此时我们加入再次传入 hello 就会直接报错提示,并且返回值 result 每次也可以得到正确的返回类型而不是一个联合类型。

命名空间

什么是命名空间呢?我们可以先看一段代码:

// page.ts
class Header {
  constructor() {
    const div = document.createElement('div')
    div.innerHTML = 'This is Header'
    document.body.appendChild(div)
  }
}
class Content {
  constructor() {
    const div = document.createElement('div')
    div.innerHTML = 'This is Content'
    document.body.appendChild(div)
  }
}
class Footer {
  constructor() {
    const div = document.createElement('div')
    div.innerHTML = 'This is Footer'
    document.body.appendChild(div)
  }
}
class Page {
  constructor() {
    new Header()
    new Content()
    new Footer()
  }
}

我们将上述代码使用 ts-node 生成 js 代码就会发现在代码中多了 4 个全局变量,分别是 HeaderContentFooterPage ,随着更多代码的加入,我们的这种写法可能会引入更多的全局变量,这样代码写到后期可能就会有很多命名冲突。就像上述栗子一样,我们其实只希望有一个全局变量直接引用 Page 就可以了,那么我们此时就可以使用 namespace 来解决:

namespace Home {
  class Header {
    constructor() {
      const div = document.createElement('div')
      div.innerHTML = 'This is Header'
      document.body.appendChild(div)
    }
  }
  class Content {
    constructor() {
      const div = document.createElement('div')
      div.innerHTML = 'This is Content'
      document.body.appendChild(div)
    }
  }
  class Footer {
    constructor() {
      const div = document.createElement('div')
      div.innerHTML = 'This is Footer'
      document.body.appendChild(div)
    }
  }
  export class Page {
    constructor() {
      new Header()
      new Content()
      new Footer()
    }
  }
}

我们将所有代码包裹在一个叫 Home 的命名空间里,此时我们在看生成的 js 代码就会发现只有一个全局变量 Home。那我们如何使用 Home 中的 Page 呢?只需要在使用类的前面加上 export 就可以了,此时回到我们的 index.html

<body>
  <script>
    new Home.Page()
  </script>
</body>

那如果我们有多个 ts 文件使用这种命名空间,两个命名空间之间是否可以互相引用呢?此时我们在 page.ts 中新建一个 content.ts 文件,将 page.ts 中的页面书写部分代码抽离出来:

// content.ts
namespace Content {
  export class Header {
    constructor() {
      const div = document.createElement('div')
      div.innerHTML = 'This is Header'
      document.body.appendChild(div)
    }
  }
  export class Content {
    constructor() {
      const div = document.createElement('div')
      div.innerHTML = 'This is Content'
      document.body.appendChild(div)
    }
  }
  export class Footer {
    constructor() {
      const div = document.createElement('div')
      div.innerHTML = 'This is Footer'
      document.body.appendChild(div)
    }
  }
}

然后在 page.ts 中直接使用 new Content 调用下面的类:

// 建议在顶部加上如下规范的引入来源,方便以后维护,知道 new Content 来自于哪个文件
/// <reference path="content.ts" />
namespace Home {
  export class Page {
    constructor() {
      new Content.Header()
      new Content.Content()
      new Content.Footer()
    }
  }
}

当然此时我们没有在 index.html 中引入打包后的 content.js,在浏览器上运行肯定是会报错的,但是因为我们是两个 ts 文件,就需要对应引入两个编译后的 js 文件,如果以后我们有 100 个 ts 文件,难道我们还要引入 100 个编译后的 js 文件吗?此时我们想将多个 ts 文件打包到一个 js 中,我们就可以在 tsconfig.json 中做出如下配置:

{
  "compilerOptions": {
    "module": "amd", // 此时不能使用 commonjs
    "outFile": "./dist/page.js", // 指定打包后生成的文件名和地址
    "outDir": "./dist", // 指定需要编译成 js 输出的文件目录
    "rootDir": "./src", // 指定需要打包的 ts 文件目录
  }
}

此时我们就将 content.tspage.ts 都打包到了 dist 目录下的 page.js 中,我们在 index.html 中引入 page.js 之后代码就可以正常运行了。

认识 Parcel

我们知道浏览器是无法直接识别 ts ,日常开发中我们基本都是通过 webpack 帮我们做打包编译,除了 webpack 我们也可以了解一下 parcel,它也是最近比较流行的 web 打包工具,但是它无需各种繁琐的配置,速度更快。

我们先使用 npm init -ytsc --init 快速初始化一个项目的相关配置,然后我们安装 parcel

npm install parcel@next -D

我们新建一个 ts 文件,然后随便写入一段 ts 语法,如下:

const username: string = 'cc'

然后新建一个 index.html 并在其中直接引入刚刚建立的 ts 文件,此时我们使用浏览器打开是会报错的,但是我们安装了 parcel,只需要在 package.json 中小小配置一下,使用 parcel 去打开 index.html 就可以在浏览器完美运行了。

{
  "scripts": {
    "test": "parcel ./index.html"
  },
}

直接运行 npm run test 它会帮我们起一个小型服务器,然后将网址输入到浏览器中,发现我们的 ts 代码就直接运行到浏览器上了,怎么样,是不是觉得它很酷呢!!!

类的装饰器

我们先跟着官网来看看它的定义:

1、类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中(.d.ts),也不能用在任何外部上下文中(比如 declare 的类)。
2、类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
3、如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
4、注意 如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中 不会为你做这些。

好吧,果然有点晦涩难懂,那我们就慢慢通过栗子来认识它,我们在搭好的环境中如果想使用装饰器,首先需要在 tsconfig.json 中将注释打开:

/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata 

我们先来认识一下装饰器:

1、装饰器本身是一个函数
2、装饰器通过 @ 符号来使用

我们先通过一个小栗子来认识一下装饰器:

function testDecorator(constructor: any) {
  console.log('decorator')
}

@testDecorator
class Test {
  constructor() { }
}

我们定义了一个函数 testDecorator ,然后通过 @testDecorator 声明了这个装饰器,类的装饰器中类的构造函数作为其唯一的参数,当我们声明类的时候装饰器就会自动执行,而不需要等到类被实例化装饰器才执行。

此时我们在装饰器上定义一个方法,然后通过实例对象来尝试调用这个方法,看是否可以获得到:

function testDecorator(constructor: any) {
  constructor.prototype.getName = () => {
    console.log('cc')
  }
}

@testDecorator
class Test {
  constructor() { }
}

const test = new Test();
(test as any).getName() // cc

虽然此时编辑器不会给我们对应的提示,但是我们确实可以直接使用实例去调用这个方法,证明我们这个装饰器成功了第一步。

当然一个类也可以使用多个装饰器,其中装饰器的收集是从上到下的,但是装饰器的执行时从下到上的。举个栗子:

function testDecorator(constructor: any) {
  console.log('111')
}

function testDecorator1(constructor: any) {
  console.log('222')
}
// 收集顺序从上到下,执行顺序从下到上
@testDecorator
@testDecorator1
class Test {
  constructor() { }
}
// 先执行 222
// 在执行 111

我们也可以在装饰器的外层包装一个函数,而函数里面只返回一个装饰器:

function testDecorator() {
  return function (constructor: any) {
    constructor.prototype.getName = () => {
      console.log(111)
    }
  }
}

@testDecorator() // 此时调用装饰器就是自调用这个函数
class Test {
  constructor() { }
}

const test = new Test();
(test as any).getName()

当我们在外面包了一层就可以传对应的参数,然后通过对应的参数判断就可以使用不同的装饰器。

function testDecorator(flag: boolean) {
  if (flag) {
    return function (constructor: any) {
      constructor.prototype.getName = () => {
        console.log(111)
      }
    }
  } else {
    return function (constructor: any) { }
  }
}
@testDecorator(true)

接下来我们再来看一种复杂的写法:

function testDecorator<T extends new (...arg: any[]) => any>(constructor: T) {
  return class extends constructor {
    name = 'wc'
  }
}

@testDecorator
class Test {
  constructor(public name: string) {
    console.log(111) // 先执行
    console.log(222)  
  }
}

const test = new Test('cc');
console.log(test) // class_1 { name: 'wc' } 后执行

我们定义了一个装饰器,它的类型为泛型,继承自一个构造函数,这个构造函数接收很多个参数,每个参数都返回 any 类型,而 T 可以通过这种构造函数的类型被实例化出来,此时我们可以理解为 T 是一个类。上述代码中我们使用 testDecorator 装饰了一个类 Test,此时会优先执行 Test 里面的构造函数,当 Test 中的构造函数被执行之后再会去执行装饰器中的。当然上述代码还不是我们想要的最终结果,因为即使这样我们在装饰器上定义的方法不会直接被挂载到类的实例上。所以我们在来改造一下这段代码:

function testDecorator() { // 将装饰器封装到一个函数中
  return function <T extends new (...arg: any[]) => any>(constructor: T) {
    return class extends constructor {
      name = 'wc';
      getName() {
        return this.name
      }
    }
  }
}
const Test = testDecorator()( // 执行函数之后将类以参数的形式直接传到装饰器中
  class {
    constructor(public name: string) {
    }
  }
)
const test = new Test('cc');
console.log(test.getName())

上述我们直接将类传入到装饰器中,那么生成的实例就可以直接调用装饰器里的方法。类的装饰器有没有觉得有点复杂,那接下来我们认识简单点的方法装饰器。

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2、成员的名字。
3、成员的属性描述符。

看了基本定义之后我们先来写一个简单的方法装饰器:

function getNameDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log('target', target)
  console.log('key', key)
  console.log('descriptor', descriptor)
}

class Test {
  constructor(public name: string) { }
  @getNameDecorator
  getName() {
    return this.name
  }
}

const test = new Test('cc')

// 对应打印的值
target Test { getName: [Function] }
key getName
descriptor {
  value: [Function],
  writable: true,
  enumerable: true,
  configurable: true
}

关于 descriptor 的只是可点击参考,当然如果我们在 Test 中的 getName 方法前面加上 static 将其变成一个静态方法,那么 target 就是类的构造函数:

function getNameDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log('target', target) // target [Function: Test] { getName: [Function] }
}

class Test {
  constructor(public name: string) { }
  @getNameDecorator
  static getName() {
    return 111
  }
}

这里我么简单的使用 descriptor 来在装饰器中改变类中的方法返回值:

function getNameDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
  descriptor.value = function () {
    return 222
  }
}
class Test {
  constructor(public name: string) { }
  @getNameDecorator
  getName() {
    return 111
  }
}
const test = new Test('cc')
console.log(test.getName())
访问器装饰器

访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。

TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。

先来回顾一下访问器,啥是访问器?

class Test {
  constructor(private _name: string) { }
  get name() {
    return this._name
  }
  set name(name: string) {
    this._name = name
  }
}
const test = new Test('cc')
test.name = 'wc' // 调用 set 
console.log(test.name) // 调用 get

访问器的装饰器和方法的装饰器很像,接收的参数基本一样,我们来讲上述代码写上装饰器:

function visitDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log(target)
  console.log(key)
  console.log(descriptor)
}
class Test {
  constructor(private _name: string) { }
  get name() {
    return this._name
  }
  @visitDecorator
  set name(name: string) {
    this._name = name
  }
}
const test = new Test('cc')
test.name = 'wc' // 调用 set 
console.log(test.name) // 调用 get
属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2、成员的名字。

function nameDecorator(target: any, key: string) {
  console.log(target)
  console.log(key)
}

class Test {
  @nameDecorator
  name = 'cc';
}

const test = new Test()
console.log(test.name)

我们无法直接使用装饰器的 target 去修改属性的值,因为 target 指向的是类的原型对象,而 name 则是直接挂在实例上的。

function nameDecorator(target: any, key: string) {
  target[key] = 'wc'
}

class Test {
  @nameDecorator
  name = 'cc';
}

const test = new Test()
console.log(test.name) // cc
console.log((test as any).__proto__.name) 
// 无法直接修改,只是在原型上添加了修改,无法直接在实例上访问
参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2、成员的名字。
3、参数在函数参数列表中的索引。

来通过实际栗子看看:

function paramDecorator(target: any, method: string, paramIndex: number) {
  console.log(target, method, paramIndex)
  // Test { getInfo: [Function] } getInfo 1
}

class Test {
  getInfo(name: string, @paramDecorator age: number) {
    console.log(name, age) // cc 18
  }
}

const test = new Test()
console.log(test.getInfo('cc', 18))

看了这么多装饰器,实战中到底怎么用呢?我们通过一个小栗子迅速来了解一下吧:

const userInfo: any = undefined
class Test {
  getName() {
    return userInfo.name
  }
  getAge() {
    return userInfo.name
  }
}
const test = new Test()
console.log(test.getName())

上述代码中我们 userInfo 的值为 undefined,所以我们在类的方法中直接调用会报错,聪明的你肯定知道使用 try catch 来预防报错:

const userInfo: any = undefined

class Test {
  getName() {
    try {
      return userInfo.name
    } catch (e) {
      console.log('userInfo.name 不存在')
    }
  }
  getAge() {
    try {
      return userInfo.age
    } catch (e) {
      console.log('userInfo.age 不存在')
    }
  }
}
const test = new Test()
console.log(test.getName())

如果我们有不同的方法要调用 userInfo ,此时我们是不是要写很多遍 try catch ,那我们用方法装饰器改造一下:

const userInfo: any = undefined
function catchError(target: any, key: string, descriptor: PropertyDescriptor) {
  const fn = descriptor.value;
  descriptor.value = function () {
    try {
      fn()
    } catch (e) {
      console.log('userInfo 不存在')
    }
  }
}
class Test {
  @catchError
  getName() {
    return userInfo.name
  }
  @catchError
  getAge() {
    return userInfo.age
  }
}
const test = new Test()
console.log(test.getName())
console.log(test.getAge())

那么如果我们希望返回的不是 userInfo 不存在 而是它下面对应的属性不存在,那么我们就可以用工厂模式改装一下这个装饰器:

const userInfo: any = undefined
function catchError(msg: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const fn = descriptor.value;
    descriptor.value = function () {
      try {
        fn()
      } catch (e) {
        console.log(msg)
      }
    }
  }
}
class Test {
  @catchError('userInfo.name 不存在')
  getName() {
    return userInfo.name
  }
  @catchError('userInfo.age 不存在')
  getAge() {
    return userInfo.age
  }
}
const test = new Test()
console.log(test.getName())
console.log(test.getAge())

当然这只是一个小栗子初步了解,后面我们还会用更多案例来用到这些知识。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容

  • 一、简介 1.1 什么是 TypeScript TypeScript 是 JavaScript 的一个超集,主要提...
    _ihhu阅读 1,199评论 0 2
  • 最近公司项目开始统一使用Typescript了,所以自己也是开始认真的学习了一下typescript,还是很有收获...
    认真学前端阅读 963评论 0 2
  • 一、基础部分 1.安装和编译 2.用VScode 自动编译 .ts a. 创建tsconfig.json文件。 t...
    _前端码农_阅读 839评论 0 1
  • Vue3 基本全部使用 TypeScript 来进行重写,尽管你可能觉得要学的东西越来越多了,但是作为程序员,如果...
    橙色流年阅读 609评论 0 1
  • Typescript优点之一:增强的oo,可以用更多设计模式,IoC,AOP... class js中,使用函数和...
    前端精阅读 393评论 0 1