聯絡我們:有道技術團隊助手:ydtech01 / 郵箱:ydtech@rd.netease.com
本文是《玩轉TypeScript工具型別》系列的最後一篇,包含了如下幾部分內容:
- ThisParameterType<Type>
- OmitThisParameter<Type>
- ThisType<Type>
快捷跳轉
一. ThisParameterType<Type>
提取一個函式型別顯式定義的 this 引數,如果沒有顯式定義的 this 引數,則返回 unknown 。 這裡有如下幾個需要注意的點:
- this引數只能叫 this,且必須在引數列表的第一個位置
- this 必須是顯式定義的
- 這個 this 引數在函式實際被呼叫的時候不存在,不需要顯式作為引數傳入,而是通過 call、apply或者是 bind 等方法指定
1.1 原始碼解析
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 型別引數 U ,false 分支就返回 unknown。
1.2 實戰用法
顯式的定義 this 型別有助於我們在函式內部安全的使用 this 。
function toHex(this: Number) {
return this.toString(16);
}
function numberToString(n: ThisParameterType<typeof toHex>) {
return toHex.apply(n);
}
注:定義了一個函式,要使用這個函式的型別,可以直接使用 typeof [funcName] ,可以省去額外再定義一個型別宣告。
二. OmitThisParameter<Type>
有了 ThisParameterType獲取 this 的型別,那麼如何將一個定義了 this 引數型別的函式型別中的this 引數型別去掉呢? 這就是 OmitThisParameter 做的事情。
一句話概括,就是對於沒有定義 this 引數型別的函式型別,直接返回這個函式型別,如果定義了 this 引數型別,就返回一個僅是去掉了 this 引數型別的新函式型別。
2.1 原始碼解析
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 了。
2.2 實戰用法
function toHex(this: Number) {
return this.toString(16);
}
const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);
console.log(fiveToHex());
三. 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;
}
}
}
我們希望在 moveBy 的 this 物件上可以直接獲取到 data 物件中的 x和y 。為了實現這個功能,我們需要對定義的資料結構做一些處理,讓 methods 和 data 中的屬性共享同一個 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;
}
}
})
方法也很簡單,就是把 data 和 methods 展開,放在同一個物件 options 中,當我們通過options.moveBy() 的方式呼叫 moveBy 的時候,moveBy 的 this 就是這個物件。 功能實現了,那麼如何實現型別安全呢?接下來就需要在 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 物件已經可以取到 x 和 y 了,最後一步就是明確告訴 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工具型別》系列總計三篇就全部完成了,寫這個系列其實就是想記錄自己學習過程中的一些學習思路和感受,同時通過文章的方式寫下來加深自己的理解,所以如果有任何錯誤的地方歡迎批評指正。