TypeScript逆變之條件推斷和泛型的應(yīng)用示例詳解
一個(gè)類(lèi)型問(wèn)題
有一個(gè)名為 test 的函數(shù),它接受兩個(gè)參數(shù)。第一個(gè)參數(shù)是函數(shù) fn,第二個(gè)參數(shù) options 受到 fn 參數(shù)的限制。乍一看,這個(gè)問(wèn)題貌似并不復(fù)雜,不是嗎?糊業(yè)務(wù)的時(shí)候,這種不是常見(jiàn)的需求嘛。
“創(chuàng)建一個(gè)泛型類(lèi)型 Test,以確保這兩個(gè)參數(shù)之間存在約束關(guān)系就完事了,睡醒再說(shuō)”,就這樣暗忖著,又昏昏沉沉睡過(guò)去,只有那 T extends unknown[]闖入我夢(mèng)中,飄忽不定,若即若離,暗示著我再次翻車(chē)【看題時(shí)覺(jué)得簡(jiǎn)單,解題時(shí)頭大如牛】的命運(yùn)。
下面我們先來(lái)看看題目:
type InjectorFunction<P> = () => P;
interface Options<P> {
injector: InjectorFunction<P>;
}
const fn1 = () => 1;
const fn2 = (p: number) => `number is: ${p}!`;
const fn3 = (p: string) => `hello ${p}!`;
const fn4 = (p?: string) => `hello ${p || 'fn4'}!`;
type Test<F extends (...args: any[]) => any = any> = (fn: F, options?: Options<Parameters<F>>) => void;
const test: Test = (fn, options) => {
return fn(options?.injector?.());
}
// 定義 Test 函數(shù)的類(lèi)型,使得下面類(lèi)型成立
test(fn1); // right
test(fn1, { injector: () => {} }); // error, dont need injector
test(fn2, { injector: () => 4 }); // right
test(fn3, { injector: () => 'world' }); // right
test(fn3); // error, options.injector is required
test(fn4); // right
test(fn4, { injector: () => 'test4' }); // right在繼續(xù)往下翻閱之前,先來(lái)typescript playground 玩呀,兄弟們。也可以用來(lái)配合本文食用哦。
題目規(guī)則和解法
閱讀代碼中的注釋?zhuān)覀兛梢缘贸鲆韵骂}目描述和要求:
考慮函數(shù) test,它有兩個(gè)參數(shù)。第一個(gè)參數(shù)必然是一個(gè)函數(shù) fn,而第二個(gè)參數(shù) options 受到 fn 約束,其泛型參數(shù)是 fn的參數(shù)類(lèi)型。
- 如果
fn沒(méi)有參數(shù),則test不能有第二個(gè)參數(shù)options。 - 如果
fn有一個(gè)參數(shù)p,則test必須有第二個(gè)參數(shù)options。 - 如果
fn的參數(shù)p是可選的,則第二個(gè)參數(shù)options也是可選的。 options是個(gè)泛型Options<T>,T的類(lèi)型是fn的參數(shù)p的類(lèi)型。
在觀察前三個(gè)規(guī)則后,我們初步得出了一個(gè)類(lèi)似于下面結(jié)構(gòu)的 test 函數(shù),其中推斷參數(shù)個(gè)數(shù)的部分需要延遲:
type Test = (...arg: unknown[]) => unknown
我們知道,使用泛型類(lèi)型或條件類(lèi)型可以幫助實(shí)現(xiàn)參數(shù)之間的約束關(guān)系。而題目中已經(jīng)定義好的Test類(lèi)型中,type Test<F extends (...args: any[]) => any = any> = (fn: F, options?: Options<Parameters<F>>) => void;options直接定義為可選的,并不能符合第一和第二條規(guī)則。
我們需要?jiǎng)?chuàng)建一個(gè)名為 Args<T> 的工具類(lèi)型,它用于動(dòng)態(tài)生成 test 函數(shù)的參數(shù)。盡管我們目前使用泛型來(lái)描述這些參數(shù),但是我們可以使用偽代碼 [FN, Opts] 來(lái)暫時(shí)表示未完成的實(shí)現(xiàn)。具體而言,我們將 fn 參數(shù)的類(lèi)型稱(chēng)為 FN,將 options 參數(shù)的類(lèi)型稱(chēng)為 Opts。
type Test = <T>(...arg: Args<T>) => unknown
首先, T 必須是個(gè)數(shù)組,如果不是數(shù)組,那它就沒(méi)存在的必要了,如果是,我們先返回兩個(gè)參數(shù)組成的數(shù)組好不啦?,F(xiàn)在,可以用上前面起的小名了!略西!
type Args<T> = T extends unknown[] ? [FN, Opts] : never
其次,第一個(gè)參數(shù)必然是 fn,我們需要判斷它的參數(shù)形狀。先從最簡(jiǎn)單的 fn 沒(méi)有參數(shù)開(kāi)始。
type Args<T> = T extends unknown[] ?
T[0] extends () => number ? [() => number]: [FN, Opts] : never下一步,我們需要判斷 T[0] 是個(gè)帶有參數(shù)的函數(shù)。T[0] 是 (arg: SomeType) => unknown嗎?如果是,我們還要把 SomeType 添加到 [FN, Opts]。還記得前文第四個(gè)規(guī)則嗎,小 Opts 是個(gè)泛型,是個(gè)參數(shù)和 FN參數(shù)一致的泛型。
在條件類(lèi)型表達(dá)式中,infer 關(guān)鍵字用來(lái)聲明一個(gè)待推斷的類(lèi)型變量,將其用于 extends 條件語(yǔ)句中。這樣可以使 TypeScript 推斷出特定位置的類(lèi)型,并將其應(yīng)用于類(lèi)型判斷和條件分支中。
因此,我們可以用這個(gè)條件語(yǔ)句 T[0] extends (arg: infer P) => string 來(lái)表示T[0] 可以賦值給 (arg: infer P) => string。在這個(gè)條件語(yǔ)句中,我們使用 infer P 來(lái)聲明一個(gè)類(lèi)型變量 P,它用于描述 fn 的參數(shù)類(lèi)型以及 Options<T> 泛型的參數(shù)類(lèi)型。
type Args<T> =
T extends unknown[] ?
T[0] extends () => number ? [() => number]:
T[0] extends (arg: infer P) => string ? [(arg: P) => string, Options<P>] : [FN, Opts]
: never在這一步,我們還需要解決一個(gè)問(wèn)題,即如何判斷參數(shù)是否為可選類(lèi)型。
要獲取函數(shù)的參數(shù),我們可以使用 TypeScript 內(nèi)置的 Parameters 類(lèi)型。
Parameters<T> 類(lèi)型接受一個(gè)函數(shù)類(lèi)型 T,并返回該函數(shù)類(lèi)型的參數(shù)類(lèi)型元組。通過(guò)檢查 Parameters<T> 元組的長(zhǎng)度和元素類(lèi)型,我們可以判斷參數(shù)的個(gè)數(shù)和類(lèi)型,并根據(jù)需要進(jìn)行相應(yīng)處理。
type GetParamsNum<T extends (...args: any) => any> = Parameters<T>['length'];
要判斷參數(shù)形狀是哪種,即有、無(wú)或薛定諤的有/無(wú)(即參數(shù)個(gè)數(shù)可以是 0,也可以是 1,或者是 0 | 1),我們可以使用以下代碼來(lái)區(qū)分這三種情況:0,1,0 | 1。
type GetParamShape<T> = [T] extends [0] ? "無(wú)" : [T] extends [1] ? "有" : "薛定諤的有/無(wú)"
綜上所述,讓我們進(jìn)一步分解這個(gè)分支:T[0] extends (arg: infer P) => string,Args 類(lèi)型已經(jīng)完全展開(kāi),我們可以得到以下結(jié)論:
- 當(dāng)
T[0]能夠賦值給(arg: infer P) => string時(shí),我們可以推斷出參數(shù)類(lèi)型P是函數(shù)T[0]的參數(shù)類(lèi)型。 - 通過(guò)
Parameters<T[0]>,我們可以獲取函數(shù)T[0]的參數(shù)類(lèi)型元組。 - 通過(guò)判斷
[Parameters<T[0]>['length']] extends[1],我們得到函數(shù)T[0]必然有一個(gè)參數(shù)的分支,從而返回預(yù)期的類(lèi)型[(arg: P) => string, Options<P>]。 - 如果條件不符合,返回預(yù)期的類(lèi)型
[(arg?: P) => unknown, Options<P>?], arg是可選的,Options也是可選的。
Args 類(lèi)型的完整定義如下:
type Args<T> = T extends unknown[] ? T[0] extends () => number ? [() => number]: T[0] extends (arg: infer P) => string ? [Parameters<T[0]>['length']] extends[1] ? [(arg: P) => string, Options<P>] : [(arg?: P) => unknown, Options<P>?] : never : never
現(xiàn)在,根據(jù)前面的 type Test = <T>(...arg: Args<T>) => unknown,讓我們對(duì) test 函數(shù)進(jìn)行進(jìn)一步改造。
type Test = <T>(...arg: Args<T>) => unknown
const test: Test = (...args) => {
const [fn, options] = args
return fn(options?.injector?.())
}在這個(gè)改造后的 test 函數(shù)中,我們接受一個(gè)參數(shù)數(shù)組 args,其中包含了函數(shù) fn 和 options 參數(shù)。我們使用數(shù)組解構(gòu)賦值將這兩個(gè)參數(shù)提取出來(lái)。
我們已經(jīng)完成了類(lèi)型定義的重新定義以及函數(shù)的改造,現(xiàn)在讓我們來(lái)看看是否能夠得到預(yù)期的類(lèi)型推斷和錯(cuò)誤。
第一次翻車(chē)
每個(gè)調(diào)用都報(bào)錯(cuò)了。一個(gè)方案是在調(diào)用的時(shí)候指定泛型參數(shù),但這樣做就很麻煩,并且毫不意外地被大佬嫌棄了。那就開(kāi)始對(duì) Test 進(jìn)行進(jìn)一步改造。
這次的改造將進(jìn)一步簡(jiǎn)化 Args 類(lèi)型,使其看起來(lái)更加一目了然。它接受一個(gè)泛型參數(shù) T,該參數(shù)是一個(gè)數(shù)組類(lèi)型,表示函數(shù)的參數(shù)列表。根據(jù)不同的參數(shù)個(gè)數(shù),我們進(jìn)行不同的類(lèi)型轉(zhuǎn)換:
- 如果參數(shù)列表為空,即
T extends [],則表示函數(shù)沒(méi)有參數(shù)。在這種情況下,test沒(méi)有其他參數(shù),即[]。 - 如果參數(shù)列表只有一個(gè)元素
P,即T extends [infer P],則表示函數(shù)只有一個(gè)參數(shù)。我們將該參數(shù)的類(lèi)型進(jìn)行轉(zhuǎn)換為Options<P>,即一個(gè)帶有P類(lèi)型的Options類(lèi)型的元組,即[Options<P>]。 - 對(duì)于其他情況,我們將整個(gè)參數(shù)列表定義為一個(gè)可選的
Options<string>類(lèi)型的元組,即[Options<string>?]。
最后,我們定義了一個(gè) Test 類(lèi)型,它是一個(gè)高階函數(shù)類(lèi)型,接受一個(gè)函數(shù) T 作為第一個(gè)參數(shù),以及根據(jù)函數(shù)參數(shù)列表進(jìn)行轉(zhuǎn)換的元組類(lèi)型 Args<Parameters<T>>。該類(lèi)型表示函數(shù)的參數(shù)列表可能有多個(gè),并且根據(jù)參數(shù)個(gè)數(shù)的不同應(yīng)用不同的轉(zhuǎn)換類(lèi)型。現(xiàn)在,我們就可以直接傳入函數(shù) fn 和它的參數(shù)來(lái)調(diào)用 Test 函數(shù),不再需要在每次調(diào)用的時(shí)候指定 fn 類(lèi)型。
type Args<T extends unknown[]> = T extends [] ? [] : T extends [infer P] ? [Options<P>] : [Options<T[0]>?] type Test = <T extends (...arg: any[]) => unknown>(...args: [T, ...Args<Parameters<T>>]) => unknown
這里用上了any 和 unknown,給泛型T指定為帶有任意參數(shù)的函數(shù)類(lèi)型。應(yīng)該避免使用萬(wàn)能類(lèi)型 any,因?yàn)樗@過(guò)了類(lèi)型檢查,降低了類(lèi)型安全性。然而在此處,我們無(wú)法替換 any 為 unknown,類(lèi)型的位置影響逆變協(xié)變,函數(shù)參數(shù)通常處于逆變的位置,子類(lèi)型(更具體的類(lèi)型)不能賦值給父類(lèi)型(更寬泛的類(lèi)型)。而unknown 是所有類(lèi)型的父類(lèi)型。
看廣場(chǎng)吧,期待其它解法分享啊兄弟們。等你們來(lái)玩啊。
真正的規(guī)則
- 當(dāng)
fn沒(méi)有參數(shù)時(shí),options是可選的,但沒(méi)有injector字段。 - 當(dāng)
fn有參數(shù)且參數(shù)為必填時(shí),options.injector也是必填的,且injector的返回類(lèi)型為fn的參數(shù)類(lèi)型。 - 當(dāng)
fn有參數(shù)但參數(shù)為可選時(shí),options是可選的,injector也是可選的,且返回字符串。 options可能有其它屬性,但具體是什么屬性并沒(méi)有明確指定。因此,我們可以假設(shè)其他屬性只有一個(gè)weight屬性。
預(yù)期錯(cuò)誤如下所示:
// 定義 Test 函數(shù)的類(lèi)型,使得下面類(lèi)型成立
test(fn1); // right
test(fn1, { weight: 10 }); // right
test(fn1, { injector: () => {} }); // error, dont need injector
test(fn2, { injector: () => 4 }); // right
test(fn3, { injector: () => 'world' }); // right
test(fn3); // error, options.injector is required
test(fn3, { injector: () => 4 }); // error
test(fn4); // right
test(fn4, { injector: () => 'test4' }); // right
test(fn4, { injector: () => undefined }); // error為了符合上述規(guī)則,我們對(duì)泛型工具類(lèi)型 Args進(jìn)行了一些分支上的改造處理:
- 如果
fn參數(shù)列表為空,即T extends [],則剩余的參數(shù)列表定義為一個(gè)可選的OtherOpts類(lèi)型的元組,即[OtherOpts?]。 - 如果
fn參數(shù)列表只有一個(gè)元素P,即T extends [infer P]。我們將該參數(shù)的類(lèi)型進(jìn)行轉(zhuǎn)換為Options<P>,指定options.injector的返回類(lèi)型為fn參數(shù)類(lèi)型P。 - 對(duì)于其它情況,我們將整個(gè)參數(shù)列表定義為一個(gè)可選的
Options<string>類(lèi)型的元組,即[Options<string>?]。
Test 高階函數(shù)類(lèi)型保持不變。
interface OtherOpts {
weight: number;
}
type Args<T extends unknown[]> =
T extends [] ? [OtherOpts?] :
T extends [infer P] ? [Options<P>] : [Options<string>?]以上就是TypeScript逆變之條件推斷和泛型的應(yīng)用示例詳解的詳細(xì)內(nèi)容,更多關(guān)于TypeScript逆變條件推斷泛型的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
TypeScript類(lèi)型any never void和unknown使用場(chǎng)景區(qū)別
這篇文章主要為大家介紹了TypeScript類(lèi)型any never void和unknown使用場(chǎng)景區(qū)別,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
TypeScript開(kāi)發(fā)HapiJS應(yīng)用詳解
這篇文章主要為大家介紹了TypeScript開(kāi)發(fā)HapiJS應(yīng)用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
TypeScript防抖節(jié)流函數(shù)示例詳解
這篇文章主要為大家介紹了TypeScript防抖節(jié)流函數(shù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
TypeScript Module Resolution解析過(guò)程
這篇文章主要為大家介紹了TypeScript Module Resolution解析過(guò)程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
Spartacus中navigation?item?reducer實(shí)現(xiàn)解析
這篇文章主要為大家介紹了Spartacus中navigation?item?reducer實(shí)現(xiàn)解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
TypeScript十大排序算法插入排序?qū)崿F(xiàn)示例詳解
這篇文章主要為大家介紹了TypeScript十大排序算法插入排序?qū)崿F(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
TypeScript十大排序算法之選擇排序?qū)崿F(xiàn)示例詳解
這篇文章主要為大家介紹了TypeScript十大排序算法之選擇排序?qū)崿F(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02

