typescript修煉指南(三)

大綱

本章主要講解一些ts的高級用法,涉及以下內(nèi)容:

  • 類型斷言與類型守衛(wèi)
  • in關(guān)鍵詞和is關(guān)鍵詞
  • 類型結(jié)構(gòu)
  • 裝飾器 ?
  • Reflect Metadata 元編程

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

往期推薦:


類型斷言與類型守衛(wèi)

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

  • 單斷言
interface Student {
    name?: string,
    age?: number,
}
  • 雙重斷言
const sudent1 = '男' as string as Student
  • 類型守衛(wèi)
 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關(guān)鍵詞和is關(guān)鍵詞

  • in 關(guān)鍵詞 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 關(guān)鍵詞, 把參數(shù)的范圍縮小化
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')

類型結(jié)構(gòu)

  • 字面量類型
 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
  • 聯(lián)合類型
const name: string | number = '1111'  // 只能是字符串或者數(shù)字

// 聯(lián)合類型辨識
// 比如場景:  新增(無需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定義
    它和接口的用法很像但又有本質(zhì)的區(qū)別:
  1. interface 有extends 和 implements(類實現(xiàn)接口的方法)
  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 本質(zhì)就是一個函數(shù), 作用在于可以讓其它函數(shù)不在改變代碼的情況下,增加額外的功能,適合面向切面的場景,比如我去要在某個地方附加日志的功能,它最終返回的是一個函數(shù)對象。一些框架中其實也用到了裝飾器,比如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
  • 方法裝飾器
// 方法裝飾器
// 聲明裝飾器函數(shù)
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}

  
      // 打印結(jié)果,檢查是否成功修改實例方法
      console.log(xmz.say());  // 發(fā)現(xiàn)報錯了  TypeError: Cannot assign to read only property 'say' of object '#<Person>'
  • 參數(shù)裝飾器:
    參數(shù)裝飾器可以提供信息,給比如給類原型添加了一個新的屬性,屬性中包含一系列信息,這些信息就被成為「元數(shù)據(jù)」,然后我們就可以使用另外一個裝飾器來讀取「元數(shù)據(jù)」。
  1. target —— 當前對象的原型,也就是說,假設(shè) Person1 是當前對象,那么當前對象 target 的原型就是 Person1.prototype
  2. propertyKey —— 參數(shù)的名稱,上例中指的就是 get
  3. index —— 參數(shù)數(shù)組中的位置,比如上例中參數(shù) 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)
  • 裝飾器工廠 往往我們不推薦一個類身上綁定過多的裝飾器,而是希望統(tǒng)一化去處理
// 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}`
      }
  }

  // 聲明裝飾器構(gòu)造函數(shù)
  // 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
  }

  // 參數(shù)裝飾器
  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("沒找到裝飾器函數(shù)")       
      }
  }

  // 然后用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}`
      }
  }
  // 操作順序: 
  // 由上至下依次對裝飾器表達式求值。
  // 求值的結(jié)果會被當作函數(shù),由下至上依次調(diào)用。

Reflect Metadata 元編程

<span style='color:#ff502c;background: #fff5f5;'> Reflect Metadata </span>屬于 ES7 的一個提案,它的主要作用就是在聲明的時候添加和讀取元數(shù)據(jù)。目前需要引入 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'

基本參數(shù):

  • Metadata Key: 元數(shù)據(jù)的Key,本質(zhì)上內(nèi)部實現(xiàn)是一個Map對象,以鍵值對的形式儲存元數(shù)據(jù)
  • Metadata Value: 元數(shù)據(jù)的Value,這個容易理解
  • Target: 一個對象,表示元數(shù)據(jù)被添加在的對象上
  • Property: 對象的屬性,元數(shù)據(jù)不僅僅可以被添加在對象上,也可以作用于屬性,這跟裝飾器類似 --- 所作用的屬性
@Reflect.metadata('class', 'Person5')
class Person5 {
    @Reflect.metadata('method', 'say')
    say(): string {
        return 'say'
    }
}

// 獲取元數(shù)據(jù)
Reflect.getMetadata('class', Person5) // 'Person5'
Reflect.getMetadata('method', new Person5, 'say') // 'say'
// 這里為啥要new Person5 ?
// 原因就在于元數(shù)據(jù)是被添加在了實例方法上,因此必須實例化才能取出,要想不實例化,
// 則必須加在靜態(tài)方法上.
  • 內(nèi)置元數(shù)據(jù)(不是自己添加的自帶的)
// 獲取方法的類型 --- design:type 作為 key 可以獲取目標的類型
 const type = Reflect.getMetadata("design:type", new Person5, 'say') // [Function: Function]

// 獲取參數(shù)的類型,返回數(shù)組 --- design:paramtypes 作為 key 可以獲取目標參數(shù)的類型
const typeParam = Reflect.getMetadata("design:paramtypes", new Person5, 'say') // [Function: String]

// 元數(shù)據(jù)鍵獲取有關(guān)方法返回類型的信息 ----使用 design:returntype :
const typeReturn = Reflect.getMetadata("design:returntype", new Person, 'say')
// [Function: String]
實踐

實現(xiàn)以下需求: 后臺路由管理, 實現(xiàn)一個控制器Controller 來管理路由中的方法, 暫時不考慮接收請求參數(shù)

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

1, 需求肯定是需要實現(xiàn)一個Controller裝飾器工廠

const METHOD_METADATA = 'method'
const PATH_METADATA = 'path'
// 裝飾器工廠函數(shù),接收path返回對應(yīng)的裝飾器
const Controller = (path: string): ClassDecorator => {
    return target => {
        Reflect.defineMetadata(PATH_METADATA, path, target) // 為裝飾器添加元數(shù)據(jù)
    }
}

2, 接著需要實現(xiàn) Get Post 等方法裝飾器: --- 接收方法參數(shù)并返回對應(yīng)路徑的裝飾器函數(shù).實際上是一個柯里化函數(shù) ,是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù).

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')

到這里為止我們已經(jīng)可以向Class中添加各種必要的元數(shù)據(jù)了,但是我們還差一步,就是讀取元數(shù)據(jù)。

 // 判斷是否為構(gòu)造函數(shù)
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]';
}

我們需要一個函數(shù)來讀取整個Class中的元數(shù)據(jù):

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'
    //   }
    // ]
    

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容