typescript修煉指南(三)

Alleyine發表於2019-12-25

typescript修煉指南(三)

大綱

本章主要講解一些ts的高階用法,涉及以下內容:

  • 型別斷言與型別守衛
  • in關鍵詞和is關鍵詞
  • 型別結構
  • 裝飾器 ❤
  • Reflect Metadata 超程式設計

往期推薦:

這篇稍微偏難一點,本文講解(不會講)的地方不是很多,主要以例項程式碼的形式展示,重點在歸納和整理(搬筆記),建議不懂的地方查閱文件或者是搜尋 QAQ


型別斷言與型別守衛

簡單而言,做的就是確保型別更加的安全

  • 單斷言
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<T extends object, U>(obj1: T, obj2: U): T & U {
        const result = <T & U>{}; // 交叉型別
        
        for(let name in obj1) {
            (<T>result)[name] = obj1[name]
        }

        for(let name in obj2) {
            if(!result.hasOwnProperty(name)) {
                (<U>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<T> = { age: T }
const ageObj: Age<number> = { age: 20}
複製程式碼
  • 屬性自引
type Age1<T> = {
    name: number
    prop: Age1<T> // 引用自己的屬性
}
複製程式碼

裝飾器

裝飾器這裡要提一下, 最初裝飾器是在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 '#<Person>'
複製程式碼
  • 引數裝飾器: 引數裝飾器可以提供資訊,給比如給類原型新增了一個新的屬性,屬性中包含一系列資訊,這些資訊就被成為「後設資料」,然後我們就可以使用另外一個裝飾器來讀取「後設資料」。
  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'
    //   }
    // ]
    
複製程式碼

如果對大家有幫助記得點贊個~ , 如有錯誤請指正, 我們一起解決,一起進步。

相關文章