利用TypeScript從字符串字面量類型提取參數(shù)類型
正文
挑戰(zhàn)
我們先來(lái)做一個(gè)ts的挑戰(zhàn)。
你知道如何為下面的app.get
方法定義TypeScript
類型嗎?
req.params
是從傳入的第一個(gè)參數(shù)字符串中提取出來(lái)的。
當(dāng)你想對(duì)一個(gè)類似路由的函數(shù)定義一個(gè)類型時(shí),這顯得很有用,你可以傳入一個(gè)帶路徑模式的路由,你可以使用自定義語(yǔ)法格式去定義動(dòng)態(tài)參數(shù)片段(例如:[shopid]
、:shopid
),以及一個(gè)回調(diào)函數(shù),它的參數(shù)類型來(lái)源于你剛剛傳入的路由。
所以,如果你嘗試訪問(wèn)沒(méi)有定義的參數(shù),將會(huì)報(bào)錯(cuò)!
舉一個(gè)真實(shí)案例,如果你對(duì)React Router
很熟悉,應(yīng)該知道render
函數(shù)中的RouteProps
的類型是從path
參數(shù)派生出來(lái)的。
本文將探討如何定義這樣一個(gè)類型,通過(guò)各種ts技術(shù),從字符串字面量類型中提取類型。
需要掌握的內(nèi)容
首先,在我們探討之前,需要先講下一些基本的知識(shí)要求。
字符串字面量類型
ts的字符串類型是一個(gè)可以有任何值的字符串
let str: string = 'abc'; str = 'def'; // no errors, string type can have any value
而字符串字面量類型是一個(gè)具有特定值的字符串類型。
let str: 'abc' = 'abc'; str = 'def'; // Type '"def"' is not assignable to type '"abc"'.
通常情況下,我們將它與聯(lián)合類型一起使用,用來(lái)確定你可以傳遞給函數(shù)、數(shù)組、對(duì)象的字符串取值的列表。
function eatSomething(food: 'sushi' | 'ramen') {} eatSomething('sushi'); eatSomething('ramen'); eatSomething('pencil'); // Argument of type '"pencil"' is not assignable to parameter of type '"sushi" | "ramen"'. let food: Array<'sushi' | 'ramen'> = ['sushi']; food.push('pencil'); // Argument of type '"pencil"' is not assignable to parameter of type '"sushi" | "ramen"'. let object: { food: 'sushi' | 'ramen' }; object = { food: 'sushi' }; object = { food: 'pencil' }; // Type '"pencil"' is not assignable to type '"sushi" | "ramen"'.
你是如何創(chuàng)建字符串字面量類型的呢?
當(dāng)你使用const
定義一個(gè)字符串變量時(shí),它就是一個(gè)字符串字面量類型。然而,如果你用let
去定義它,ts識(shí)別出變量的值可能會(huì)改變,所以它把變量分配給一個(gè)更通用的類型:
同樣的情況對(duì)對(duì)象和數(shù)組也一樣,你可以在以后去修改對(duì)象、數(shù)組的值,因此ts分配給了一個(gè)更通用的類型。
不過(guò),你可以通過(guò)使用const斷言
向ts提示,你將只從對(duì)象、數(shù)組中讀取值,而不會(huì)去改變它。
模板字面量類型和字符串字面量類型
從ts4.1開(kāi)始,ts支持一種新的方式來(lái)定義新的字符串字面量類型,就是大家熟悉的字符串模板的語(yǔ)法:
const a = 'a'; const b = 'b'; // In JavaScript, you can build a new string // with template literals const c = `${a} $`; // 'a b' type A = 'a'; type B = 'b'; // In TypeScript, you can build a new string literal type // with template literals too! type C = `${A} ${B}`; // 'a b'
條件類型
條件類型允許你基于另一個(gè)類型來(lái)定義一個(gè)類型。在這個(gè)例子中,Collection<X>
可以是number[]
或者Set<number>
,這取決于X
的類型:
type Collection<X> = X extends 'arr' ? number[] : Set<number>; type A = Collection<'arr'>; // number[] // If you pass in something other than 'arr' type B = Collection<'foo'>; // Set<number>
你使用extends
關(guān)鍵字用來(lái)測(cè)試X
的類型是否可以被分配給arr
類型,并使用條件運(yùn)算符(condition ? a : b
)來(lái)確定測(cè)試成立的類型。
如果你想測(cè)試一個(gè)更復(fù)雜的類型,你可以使用infer
關(guān)鍵字來(lái)推斷該類型的一部分,并根據(jù)推斷的部分定義一個(gè)新類型。
// Here you are testing whether X extends `() => ???` // and let TypeScript to infer the `???` part // TypeScript will define a new type called // `Value` for the inferred type type GetReturnValue<X> = X extends () => infer Value ? Value : never; // Here we inferred that `Value` is type `string` type A = GetReturnValue<() => string>; // Here we inferred that `Value` is type `number` type B = GetReturnValue<() => number>;
函數(shù)重載和通用函數(shù)
當(dāng)你想在ts中定義一個(gè)參數(shù)類型和返回值類型相互依賴的函數(shù)類型時(shí),可以使用函數(shù)重載或者通用函數(shù)。
function firstElement(arr) { return arr[0]; } const string = firstElement(['a', 'b', 'c']); const number = firstElement([1, 2, 3]);
// return string when passed string[] function firstElement(arr: string[]): string; // return number when passed number[] function firstElement(arr: number[]): number; // then the actual implementation function firstElement(arr) { return arr[0]; } const string = firstElement(['a', 'b', 'c']);
// Define type parameter `Item` and describe argument and return type in terms of `Item` function firstElement<Item>(arr: Item[]): Item | undefined { return arr[0]; } // `Item` can only be of `string` or `number` function firstElement<Item extends string | number>(arr: Item[]): Item | undefined { return arr[0]; } const number = firstElement([1, 3, 5]); const obj = firstElement([{ a: 1 }]); // Type '{ a: number; }' is not assignable to type 'string | number'.
著手解決問(wèn)題
了解了以上知識(shí),我們對(duì)于問(wèn)題的解決方案可能可以采取這樣的形式:
function get<Path extends string>(path: Path, callback: CallbackFn<Path>): void { // impplementation } get('/docs/[chapter]/[section]/args/[...args]', (req) => { const { params } = req; });
我們使用了一個(gè)類型參數(shù)Path
(必須是一個(gè)字符串)。path
參數(shù)的類型是Path
,回調(diào)函數(shù)的類型是CallbackFn<Path>
,而挑戰(zhàn)的關(guān)鍵之處就是要弄清楚CallbackFn<Path>
。
我們計(jì)劃是這樣子的:
- 給出
path
的類型是Path
,是一個(gè)字符串字面量類型。
type Path = '/purchase/[shopid]/[itemid]/args/[...args]';
- 我們派生出一個(gè)新的類型,這個(gè)類型將字符串分解成它的各個(gè)部分。
type Parts<Path> = 'purchase' | '[shopid]' | '[itemid]' | 'args' | '[...args]';
- 篩選出只包含參數(shù)的部分
type FilteredParts<Path> = '[shopid]' | '[itemid]' | '[...args]';
- 刪除不需要的括號(hào)
type FilteredParts<Path> = 'shopid' | 'itemid' | '...args';
- 將參數(shù)映射到一個(gè)對(duì)象類型中
type Params<Path> = { shopid: any; itemid: any; '...args': any; };
- 使用條件類型來(lái)定義map的值部分
type Params<Path> = { shopid: number; itemid: number; '...args': string[]; };
- 重置鍵名,刪除
...args
中的...
type Params<Path> = { shopid: number; itemid: number; args: string[]; };
最后
type CallbackFn<Path> = (req: { params: Params<Path> }) => void;
分割字符串字面量類型
為了分割一個(gè)字符串字面量類型,我們可以使用條件類型來(lái)檢查字符串字面量的取值:
type Parts<Path> = Path extends `a/b` ? 'a' | 'b' : never; type AB = Parts<'a/b'>; // type AB = "a" | "b"
但是要接收任意字符串字面量,我們無(wú)法提前知道是什么值
type CD = Parts<'c/d'>; type EF = Parts<'e/f'>;
我們必須在條件測(cè)試中推斷出數(shù)值,并使用推斷出來(lái)的數(shù)值類型:
type Parts<Path> = Path extends `${infer PartA}/${infer PartB}` ? PartA | PartB : never; type AB = Parts<'a/b'>; // type AB = "a" | "b" type CD = Parts<'c/d'>; // type CD = "c" | "d" type EFGH = Parts<'ef/gh'>; // type EFGH = "ef" | "gh"
而如果你傳入一個(gè)不匹配模式的字符串字面量,我們希望直接返回:
type Parts<Path> = Path extends `${infer PartA}/${infer PartB}` ? PartA | PartB : Path; type A = Parts<'a'>; // type A = "a"
有一點(diǎn)需要注意,PartA
的推斷是'non-greedily'的,即:它將盡可能地進(jìn)行推斷,但不包含一個(gè)/
字符串。
type ABCD = Parts<'a/b/c/d'>; // type ABCD = "a" | "b/c/d"
因此,為了遞歸地分割Path
字符串字面量,我們可以返回Parts<PathB>
類型替代原有的PathB
類型:
type Parts<Path> = Path extends `${infer PartA}/${infer PartB}` ? PartA | Parts<PartB> : Path; type ABCD = Parts<'a/b/c/d'>; // type ABCD = "a" | "b" | "c" | "d"
以下是所發(fā)生的詳細(xì)復(fù)盤(pán):
type Parts<'a/b/c/d'> = 'a' | Parts<'b/c/d'>; type Parts<'a/b/c/d'> = 'a' | 'b' | Parts<'c/d'>; type Parts<'a/b/c/d'> = 'a' | 'b' | 'c' | Parts<'d'>; type Parts<'a/b/c/d'> = 'a' | 'b' | 'c' | 'd';
參數(shù)語(yǔ)法部分的過(guò)濾
這一步的關(guān)鍵是觀察到,任何類型與never
類型聯(lián)合都不會(huì)產(chǎn)生類型
type A = 'a' | never; // type A = "a" type Obj = { a: 1 } | never; // type Obj = { a: 1; }
如果我們可以轉(zhuǎn)換
'purchase' | '[shopid]' | '[itemid]' | 'args' | '[...args]'
成
never | '[shopid]' | '[itemid]' | never | '[...args]'
那我們就可以得到:
'[shopid]' | '[itemid]' | '[...args]'
所以,要怎么實(shí)現(xiàn)呢?
我們得再次向條件類型尋求幫助,我們可以有一個(gè)條件類型,如果它以[
開(kāi)始,以]
結(jié)尾,則返回字符串字面量本身,如果不是,則返回never
:
type IsParameter<Part> = Part extends `[${infer Anything}]` ? Part : never; type Purchase = IsParameter<'purchase'>; // type Purchase = never type ShopId = IsParameter<'[shopid]'>; // type ShopId = "[shopid]"
type IsParameter<Part> = Part extends `[${infer Anything}]` ? Part : never; type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}` ? IsParameter<PartA> | FilteredParts<PartB> : IsParameter<Path>; type Params = FilteredParts<'/purchase/[shopid]/[itemid]/args/[...args]'>; // type Params = "[shopid]" | "[itemid]" | "[...args]"
刪除括號(hào):
type IsParameter<Part> = Part extends `[${infer ParamName}]` ? ParamName : never; type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}` ? IsParameter<PartA> | FilteredParts<PartB> : IsParameter<Path>; type ParamsWithoutBracket = FilteredParts<'/purchase/[shopid]/[itemid]/args/[...args]'>;
在對(duì)象類型里做一個(gè)映射
在這一步中,我們將使用上一步的結(jié)果作為鍵名來(lái)創(chuàng)建一個(gè)對(duì)象類型。
type Params<Keys extends string> = { [Key in Keys]: any; }; const params: Params<'shopid' | 'itemid' | '...args'> = { shopid: 2, itemid: 3, '...args': 4, };
type IsParameter<Part> = Part extends `[${infer ParamName}]` ? ParamName : never; type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}` ? IsParameter<PartA> | FilteredParts<PartB> : IsParameter<Path>; type Params<Path> = { [Key in FilteredParts<Path>]: any; }; type ParamObject = Params<'/purchase/[shopid]/[itemid]/args/[...args]'>;
最終版:
type IsParameter<Part> = Part extends `[${infer ParamName}]` ? ParamName : never; type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}` ? IsParameter<PartA> | FilteredParts<PartB> : IsParameter<Path>; type ParamValue<Key> = Key extends `...${infer Anything}` ? string[] : number; type RemovePrefixDots<Key> = Key extends `...${infer Name}` ? Name : Key; type Params<Path> = { [Key in FilteredParts<Path> as RemovePrefixDots<Key>]: ParamValue<Key>; }; type CallbackFn<Path> = (req: { params: Params<Path> }) => void; function get<Path extends string>(path: Path, callback: CallbackFn<Path>) { // TODO: implement }
到此這篇關(guān)于利用TypeScript從字符串字面量類型提取參數(shù)類型的文章就介紹到這了,更多相關(guān)TS取參數(shù)類型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js判斷一個(gè)對(duì)象是數(shù)組(函數(shù))的方法實(shí)例
這篇文章主要給大家介紹了關(guān)于利用js如何判斷一個(gè)對(duì)象是數(shù)組(函數(shù))的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用JS具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12ie瀏覽器使用js導(dǎo)出網(wǎng)頁(yè)到excel并打印
簡(jiǎn)單介紹一種可以使用簡(jiǎn)單的JS來(lái)實(shí)現(xiàn)把網(wǎng)頁(yè)中的信息原樣導(dǎo)出到Excel、還可以打印的方法,需要的朋友可以參考下2014-03-03JavaScript判斷表單中多選框checkbox選中個(gè)數(shù)的方法
這篇文章主要介紹了JavaScript判斷表單中多選框checkbox選中個(gè)數(shù)的方法,涉及javascript針對(duì)checkbox復(fù)選框的遍歷與判斷技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-08-08JavaScript運(yùn)動(dòng)框架 解決防抖動(dòng)問(wèn)題、懸浮對(duì)聯(lián)(二)
這篇文章主要為大家詳細(xì)介紹了JavaScript運(yùn)動(dòng)框架的第二部分,解決防抖動(dòng)問(wèn)題、懸浮對(duì)聯(lián)問(wèn)題,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05Javascript動(dòng)態(tài)創(chuàng)建div的方法
這篇文章主要介紹了Javascript動(dòng)態(tài)創(chuàng)建div的方法,是javascript節(jié)點(diǎn)操作的典型應(yīng)用,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-02-02JS判斷傳入函數(shù)的參數(shù)是否為空(函數(shù)參數(shù)是否傳遞)
這篇文章主要介紹了JS判斷傳入函數(shù)的參數(shù)是否為空(函數(shù)參數(shù)是否傳遞),需要的朋友可以參考下2023-05-05JavaScript惰性求值的一種實(shí)現(xiàn)方法示例
這篇文章主要給大家介紹了關(guān)于JavaScript惰性求值的一種實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01簡(jiǎn)單通過(guò)settimeout看javascript的運(yùn)行機(jī)制
這篇文章主要給大家介紹了關(guān)于如何通過(guò)settimeout看javascript的運(yùn)行機(jī)制的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用javascript具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05javascript 通過(guò)封裝div方式彈出div窗體
廢話少說(shuō),此js對(duì)象是通過(guò)封裝頁(yè)面上的div,將其彈出,可以彈出多個(gè),參考了一些高人代碼,達(dá)到我要的效果。先看看效果圖。配合一css就可以很好看了。2009-10-10JavaScript高階函數(shù)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了JavaScript高階函數(shù),詳細(xì)講解了什么是高階函數(shù)和高階函數(shù)的用法,有興趣的可以了解下2017-06-06