玩轉TypeScript工具類型(下)

本文是《玩轉TypeScript工具類型》系列的最後一篇,包含了以下幾部份內容:vue

  • ThisParameterType<Type>
  • OmitThisParameter<Type>
  • ThisType<Type>

快捷跳轉react

14. ThisParameterType<Type>

提取一個函數類型顯式定義的this參數,若是沒有顯式定義的this參數,則返回unknown。 這裏有以下幾個須要注意的點:typescript

  • this參數只能叫this,且必須在參數列表的第一個位置
  • this必須是顯式定義的
  • 這個this參數在函數實際被調用的時候不存在,不須要顯式做爲參數傳入,而是經過call、apply或者是bind等方法指定

>> 源碼解析

type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;
複製代碼

從源碼能夠看出對於類型參數T是要嚴格匹配(this: infer U, ...args: any[]) => any格式的,因此對於this參數的名稱和位置都是固定的。剩下的邏輯就是對this參數的類型定義一個類型參數U,在extends判斷走true分支時返回this類型參數Ufalse分支就返回unknown安全

>> 實戰用法

顯式的定義this類型有助於咱們在函數內部安全的使用thismarkdown

function toHex(this: Number) {
  return this.toString(16);
}
 
function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n);
}
複製代碼

注:定義了一個函數,要使用這個函數的類型,能夠直接使用typeof [funcName],能夠省去額外再定義一個類型聲明。數據結構

15. OmitThisParameter<Type>

有了ThisParameterType獲取this的類型,那麼如何將一個定義了this參數類型的函數類型中的this參數類型去掉呢? 這就是OmitThisParameter作的事情。一句話歸納,就是對於沒有定義this參數類型的函數類型,直接返回這個函數類型,若是定義了this參數類型,就返回一個僅是去掉了this參數類型的新函數類型。app

>> 源碼解析

type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;
複製代碼

彷佛有點長,其實就是兩個嵌套的extends條件判斷,分紅兩部分就很好理解了,首先是:函數

unknown extends ThisParameterType<T> ? T : ...
複製代碼

對於傳入的函數類型T,首先使用ThisParameterType獲取this參數的類型,可能有兩種結果一種是成功拿到this參數類型並返回,另外一種是unknown。 因此若是返回的是unknown,那麼就是走true分支,直接返回T。若是不是返回的unknown,那麼就走false分支,即:工具

T extends (...args: infer A) => infer R ? (...args: A) => R : T
複製代碼

又是一個條件判斷,即只要T是一個合法的函數類型,就必定知足(...args: infer A) => infer R,剩下的就是對參數定義一個類型參數A,對返回值定義一個類型參數R,返回(...args: A) => R,這個新的函數類型已經不包含this了。oop

>> 實戰用法

function toHex(this: Number) {
  return this.toString(16);
}
 
const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);
 
console.log(fiveToHex());
複製代碼

16. ThisType<Type>

這個工具類型很是特殊,第一個特殊之處就是它的源碼定義,是一個空接口:

/**
 * Marker for contextual 'this' type
 */
interface ThisType<T> { }
複製代碼

那麼ThisType的做用是什麼呢?正如官方註釋所寫的:做爲上下文this類型的標記。

要使用ThisType必須保證noImplicitThis配置開啓,後續咱們只討論開啓的狀況

那麼如何理解這句話呢?咱們須要從實際效果來理解,先看以下這段代碼:

let demo1 = {
  a: 'lipengpeng',
  test(msg: string) {
    this;
  }
};
複製代碼

它的this類型是什麼呢?

this: {
    a: string;
    test(msg: string): void;
}
複製代碼

也能夠手動指定this類型,好比:

let demo2 = {
  a: 'lipengpeng',
  test(this:{a: string}, msg: string) {
    this;
  }
};
複製代碼

這時的this類型就是

this: {
    a: string
}
複製代碼

其實這只是理想狀況下的this類型分析,由於TypeScript是經過靜態代碼分析推斷出的類型,在實際運行階段的this是可能發生變化的,那麼咱們如何指定運行階段的this類型呢?

若是隻看如上兩種狀況,可能以爲不用ThisType也足夠了,由於TypeScript會推斷this類型,可是這只是簡單狀況,就如咱們以前提到的,運行階段的this是能夠改變的,因此僅是依賴代碼分析是沒法預測到將來的this類型的,這時候就須要藉助咱們的主角——ThisType了。 咱們繼續從實際的使用場景入手,實際開發中咱們定義一個對象有時候會給一個數據結構,就相似於Vue2.x Options API

let options = {
  data: {
    x: 0,
    y: 0
  },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx;
      this.y += dy;
    }
  }
}
複製代碼

咱們但願在moveBythis對象上能夠直接獲取到data對象中的x和y。爲了實現這個功能,咱們須要對定義的數據結構作一些處理,讓methodsdata中的屬性共享同一個this對象,所以咱們須要一個工具方法makeObject

function makeObject(config) {
  let data = config?.data || {}
  let methods = config?.methods || {}
  return {
    ...data,
    ...methods
  }
}

let options = makeObject({
  data: {
    x: 0,
    y: 0
  },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx;
      this.y += dy;
    }
  }
})
複製代碼

方法也很簡單,就是把datamethods展開,放在同一個對象options中,當咱們經過options.moveBy()的方式調用moveBy的時候,moveBythis就是這個對象。 功能實現了,那麼如何實現類型安全呢?接下來就須要在makeObject方法上作一些改動了,重點就是定義參數類型返回值類型

// 只考慮傳入makeObject的config參數只包含data和methods兩個參數
// 定義兩個泛型參數D & M來表明它們的類型
type ObjectConfigDesc<D, M> = {
  data: D
  methods: M
}

function makeObject<D, M>(config: ObjectConfigDesc<D, M>): D & M {
  let data = config?.data || {}
  let methods = config?.methods || {}
  return {
    ...data,
    ...methods
  } as D & M
}
複製代碼

此時options對象的類型已是類型安全的了。可是咱們最關心的moveBy中的this對象卻仍然會報類型警告,但咱們知道在實際的運行過程當中,moveBy中的this對象已經能夠取到xy了,最後一步就是明確告訴TypeScript這個this對象的真實類型了,很是簡單,利用ThisType

type ObjectConfigDesc<D, M> = {
  data: D
  methods: M & ThisType<D & M>
}
複製代碼

這時候再看options的類型提示已是正確的了:

let options: {
    x: number;
    y: number;
} & {
    moveBy(dx: number, dy: number): void;
}
複製代碼

你們能夠在TypeScript Playground親手試一試,感覺會更深入一些。 注意:ThisType僅支持在對象字面量的上下文中使用,在其餘地方使用做用等同於空接口。

結束語

截止到這裏,《玩轉TypeScript工具類型》系列總計三篇就所有完成了,寫這個系列其實就是想記錄本身學習過程當中的一些學習思路和感覺,同時經過文章的方式寫下來加深本身的理解,因此若是有任何錯誤的地方歡迎批評指正。

後續會寫一些關於@vue/reactivity的源碼分享和平常業務開發中的Vue Composition Utilities的實踐,敬請期待~

相關文章
相關標籤/搜索