typescript修炼指南(三)

大纲

本章主要讲解一些ts的高级用法,涉及以下内容:

  • 类型断言与类型守卫
  • in关键词和is关键词
  • 类型结构
  • 装饰器 ❤
  • Reflect Metadata 元编程

这篇稍微偏难一点,本文讲解(不会讲)的地方不是很多,主要以实例代码的形式展示,重点在归纳和整理(搬笔记),建议不懂的地方查阅文档或者是搜索 QAQ

往期推荐:

  • typescript修炼指南(一)
  • typescript修炼指南(二)

类型断言与类型守卫

简单而言,做的就是确保类型更加的安全

  • 单断言
interface Student {
    name?: string,
    age?: number,
}
  • 双重断言
const sudent1 = '男' as string as Student
  • 类型守卫
 class Test1 {
    name = 'lili'
    age = 20
 }

class Test2 {
    sex = '男'
 }

function test(arg: Test1 | Test2) {
    if(arg instanceof Test1) {
        console.log(arg.age, arg.name)
    }

    if(arg instanceof Test2) {
        console.log(arg.sex)
    }
}

in关键词和is关键词

  • in 关键词 x属性存在于y中
 function test1(arg: Test1 | Test2) {
        if('name' in arg) {
            console.log(arg.age, arg.name)
        }

        if('sex' in arg) {
            console.log(arg.sex)
        }
    }
  • is 关键词, 把参数的范围缩小化
function user10(name: any): name is string { //  is 是正常的没报错
        return name === 'lili'
    }

    function user11(name: any): boolean { 
        return name === 'lili'
    }

function getUserName(name: string | number) {
    if(user10(name)) {
        console.log(name)
        console.log(name.length)
        // 换成boolean就会报错 user11(name)
        // Property 'length' does not exist on type 'string | number'.
        // Property 'length' does not exist on type 'number'.ts(
    }
}

getUserName('lili')

类型结构

  • 字面量类型
 type Test = {
        op: 'test', // 字面量类型
        name: string,
    }

function test2(arg: Test) {
    if(arg.op === 'test') {
        console.log(arg.name)
    }
}
  • 交叉类型,在ts中使用混入模式(传入不同对象,返回拥有所有对象属性)需要使用交叉类型
function test3(obj1: T, obj2: U): T & U {
        const result = {}; // 交叉类型
        
        for(let name in obj1) {
            (result)[name] = obj1[name]
        }

        for(let name in obj2) {
            if(!result.hasOwnProperty(name)) {
                (result)[name] = obj2[name]
            }
        }

        return result
}

const o = test3({name: 'lili'}, {age: 20})
// o.name  o.age --- ok
  • 联合类型
const name: string | number = '1111'  // 只能是字符串或者数字

// 联合类型辨识
// 比如场景:  新增(无需id) 和 查询(需要id)
type List = | { 
    action: 'add',
    form: {
        name: string,
        age: number,
    }
} | {
    action: 'select',
    id: number,
}

const getInfo = (arg: List) => {
    if(arg.action === 'add') {
        // .... ad 
    }else if(arg.action === 'select') {
        // .... select
    }
}

getInfo({action: 'select', id: 0})
  • 类型别名 type定义
    它和接口的用法很像但又有本质的区别:
  1. interface 有extends 和 implements(类实现接口的方法)
  2. interface 接口合并声明
type age = number
const p: age = 20

// 泛型中的运用
type Age = { age: T }
const ageObj: Age = { age: 20}
  • 属性自引
type Age1 = {
    name: number
    prop: Age1 // 引用自己的属性
}

装饰器

装饰器这里要提一下, 最初装饰器是在python中使用的,在java中叫注解,后来js中也慢慢运用起来了,不过要借助打包工具。说一下这个装饰器是干嘛?从字面意思上理解,装饰,就是为其赋予。比如装房子,或者打扮自己。

Decorator 本质就是一个函数, 作用在于可以让其它函数不在改变代码的情况下,增加额外的功能,适合面向切面的场景,比如我去要在某个地方附加日志的功能,它最终返回的是一个函数对象。一些框架中其实也用到了装饰器,比如nest.js框架 angular框架 还有react的一些库等等, 如果你看到 @func 这样的代码,无疑就是它了。

为什么要这里提一下ts中的装饰器呢,因为它会赋予更加安全的类型,使得功能更完备,另外可以在ts中直接被编译。

  • 类装饰器
  function addAge(constructor: Function) {
        constructor.prototype.age = 18;
      }
      
@addAge
class Person_{
    name: string;
    age: number;
    constructor() {
      this.name = 'xiaomuzhu';
      this.age = 20
    }
}
  
let person_ = new Person_();
  
console.log(person_.age); // 18
  • 方法装饰器
// 方法装饰器
// 声明装饰器函数
function decorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target);
    console.log("prop " + propertyKey);
    console.log("desc " + JSON.stringify(descriptor) + "\n\n");
    descriptor.writable = false; // 禁用方法的: 可写性 意味着只能只读 
}

class Person{
    name: string;
    constructor() {
      this.name = 'lili';
    }
  
    @decorator
    say(){
      return 'say';
    }
  
    @decorator
    static run(){
      return 'run';
    }
  }
  
  const xmz = new Person();
  
  // 修改实例方法say
  xmz.say = function() {
    return 'say'
  }

//   Person { say: [Function] }
//   prop say
//   desc {"writable":true,"enumerable":true,"configurable":true}


//   [Function: Person] { run: [Function] }
//   prop run
//   desc {"writable":true,"enumerable":true,"configurable":true}

  
      // 打印结果,检查是否成功修改实例方法
      console.log(xmz.say());  // 发现报错了  TypeError: Cannot assign to read only property 'say' of object '#'
  • 参数装饰器:
    参数装饰器可以提供信息,给比如给类原型添加了一个新的属性,属性中包含一系列信息,这些信息就被成为「元数据」,然后我们就可以使用另外一个装饰器来读取「元数据」。
  1. target —— 当前对象的原型,也就是说,假设 Person1 是当前对象,那么当前对象 target 的原型就是 Person1.prototype
  2. propertyKey —— 参数的名称,上例中指的就是 get
  3. index —— 参数数组中的位置,比如上例中参数 name 的位置是 1, message 的位置为 0
 function decotarots(target: object, propertyKey: string, index: number) {
        console.log(target, propertyKey, index)
  }

  class Person1 {
      get(@decotarots name: string, @decotarots age: number): string {
        return `name: ${name} age: ${age}`
      }
  }

  const person = new Person1()
  person.get('lili', 20)
  • 装饰器工厂 往往我们不推荐一个类身上绑定过多的装饰器,而是希望统一化去处理
// 1. 本来的代码
@DecoratorClass
  class Person2 {
      @DecoratorProp
      public name: string
      @DecoratorProp
      public age: number

      constructor(name: string, age: number) {
          this.name = name
          this.age = age
      }

      @DecoratorMethod
      public get(@DecoratorArguments name: string, @DecoratorArguments age: number): string {
        return `name: ${name} age: ${age}`
      }
  }

  // 声明装饰器构造函数
  // class 装饰器
  function DecoratorClass(target: typeof Person2) {
      console.log(target) // [Function: Person2]
  }

  // 属性装饰器
  function DecoratorProp(target: any, propertyKey: string) {
    console.log(propertyKey) // name  age
  }

  // 方法装饰器
  function DecoratorMethod(target: any, propertyKey: string) {
    console.log(propertyKey) // get
  }

  // 参数装饰器
  function DecoratorArguments(target: object, propertyKey: string, index: number) {
    console.log(index) // 0
  }
// 2. 改造后的代码
function log(...args: any) {
      switch(args.length) {
          case 1:
              return DecoratorClass.apply(this, args)
          case 2: 
              return DecoratorMethod.apply(this, args)
          case 3:
              if(typeof args[2] === "number") {
                return DecoratorArguments.apply(this, args)
              }
              return DecoratorMethod.apply(this, args) //也有可能是 descriptor: PropertyDescriptor 属性
          default:
              throw new Error("没找到装饰器函数")       
      }
  }

  // 然后用log代替即可
   @log
  class Person3 {
      @log
      public name: string
      @log
      public age: number

      constructor(name: string, age: number) {
          this.name = name
          this.age = age
      }

      @log
      public get(@log name: string, @log age: number): string {
        return `name: ${name} age: ${age}`
      }
  }
  • 同一声明-多个装饰器
class Person4 {
      // 声明多个装饰器
      @log
      @DecoratorMethod
      public get(@log name: string, @log age: number): string {
        return `name: ${name} age: ${age}`
      }
  }
  // 操作顺序: 
  // 由上至下依次对装饰器表达式求值。
  // 求值的结果会被当作函数,由下至上依次调用。

Reflect Metadata 元编程

Reflect Metadata 属于 ES7 的一个提案,它的主要作用就是在声明的时候添加和读取元数据。目前需要引入 npm 包才能使用,另外需要在 tsconfig.json 中配置 emitDecoratorMetadata.

npm i reflect-metadata --save

QAQ 这就变得和java中的注解很像很像了....
作用: 可以通过装饰器来给类添加一些自定义的信息,然后通过反射将这些信息提取出来,也可以通过反射来添加这些信息

@Reflect.metadata('name', 'A')
class A {
  @Reflect.metadata('hello', 'world')
  public hello(): string {
    return 'hello world'
  }
}
    
Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'

基本参数:

  • Metadata Key: 元数据的Key,本质上内部实现是一个Map对象,以键值对的形式储存元数据
  • Metadata Value: 元数据的Value,这个容易理解
  • Target: 一个对象,表示元数据被添加在的对象上
  • Property: 对象的属性,元数据不仅仅可以被添加在对象上,也可以作用于属性,这跟装饰器类似 --- 所作用的属性
@Reflect.metadata('class', 'Person5')
class Person5 {
    @Reflect.metadata('method', 'say')
    say(): string {
        return 'say'
    }
}

// 获取元数据
Reflect.getMetadata('class', Person5) // 'Person5'
Reflect.getMetadata('method', new Person5, 'say') // 'say'
// 这里为啥要new Person5 ?
// 原因就在于元数据是被添加在了实例方法上,因此必须实例化才能取出,要想不实例化,
// 则必须加在静态方法上.
  • 内置元数据(不是自己添加的自带的)
// 获取方法的类型 --- design:type 作为 key 可以获取目标的类型
 const type = Reflect.getMetadata("design:type", new Person5, 'say') // [Function: Function]

// 获取参数的类型,返回数组 --- design:paramtypes 作为 key 可以获取目标参数的类型
const typeParam = Reflect.getMetadata("design:paramtypes", new Person5, 'say') // [Function: String]

// 元数据键获取有关方法返回类型的信息 ----使用 design:returntype :
const typeReturn = Reflect.getMetadata("design:returntype", new Person, 'say')
// [Function: String]
实践

实现以下需求: 后台路由管理, 实现一个控制器Controller 来管理路由中的方法, 暂时不考虑接收请求参数

@Controller('/list')
class List {
    @Get('/read')
    readList() {
      return 'hello world';
    }
    
    @Post('/edit')
    editList() {}
}

1, 需求肯定是需要实现一个Controller装饰器工厂

const METHOD_METADATA = 'method'
const PATH_METADATA = 'path'
// 装饰器工厂函数,接收path返回对应的装饰器
const Controller = (path: string): ClassDecorator => {
    return target => {
        Reflect.defineMetadata(PATH_METADATA, path, target) // 为装饰器添加元数据
    }
}

2, 接着需要实现 Get Post 等方法装饰器: --- 接收方法参数并返回对应路径的装饰器函数.实际上是一个柯里化函数 ,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数.

const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
    return (target, key, descriptor) => {
        Reflect.defineMetadata(PATH_METADATA, path, descriptor.value!)
        Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value!)
    } 
}

const GET = createMappingDecorator('GET')
const POST = createMappingDecorator('POST')

到这里为止我们已经可以向Class中添加各种必要的元数据了,但是我们还差一步,就是读取元数据。

 // 判断是否为构造函数
function isConstructor(f: any): boolean {
    try {
        new f();
    } catch (err) {
    // verify err is the expected error and then
        return false;
    }
    return true;
}

function isFunction(functionToCheck: any): boolean {
    return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}

我们需要一个函数来读取整个Class中的元数据:

function mapRoute(instance: Object) {
    const prototype = Object.getPrototypeOf(instance)

    // 筛选出类的 methodName
    const methodsNames = Object.getOwnPropertyNames(prototype)
    .filter(item => !isConstructor(item) && isFunction(prototype[item]));
    return methodsNames.map(methodName => {
        const fn = prototype[methodName];

        // 取出定义的 metadata
        const route = Reflect.getMetadata(PATH_METADATA, fn);
        const method = Reflect.getMetadata(METHOD_METADATA, fn);
        
        return {
            route,
            method,
            fn,
            methodName
        }
    })
}

使用:

@Controller('/list')
class Articles {
    @GET('/read')
   readList() {
    return 'hello world';
   }
    
    @POST('/edit')
    editList() {}
}

Reflect.getMetadata(PATH_METADATA, Articles)

const res = mapRoute(new Articles())

console.log(res);

   // [
    //   {
    //     route: '/list',
    //     method: undefined,
    //     fn: [Function: Articles],
    //     methodName: 'constructor'
    //   },
    //   {
    //     route: '/read',
    //     method: 'GET',
    //     fn: [Function],
    //     methodName: 'readList'
    //   },
    //   {
    //     route: '/edit',
    //     method: 'POST',
    //     fn: [Function],
    //     methodName: 'editList'
    //   }
    // ]
    

如果对大家有帮助记得点赞个~ , 如有错误请指正, 我们一起解决,一起进步。

你可能感兴趣的:(typescript修炼指南(三))