關(guān)于C++ TpeScript系列的泛型
前言:
我在面試的時(shí)候,通常喜歡問(wèn)候選人一些莫名其妙的問(wèn)題。比如這樣的問(wèn)題,假如你是某個(gè)庫(kù)的作者,你如何實(shí)現(xiàn)某個(gè)功能。這類問(wèn)題一般沒(méi)有正確的答案,主要意圖是考察一下候選人對(duì)這個(gè)庫(kù)有沒(méi)有更深入的理解,次要意圖是覺(jué)得這樣挺好玩。玩歸玩,但該嚴(yán)肅的時(shí)候也要嚴(yán)肅起來(lái)。有一次,我面試到一位用過(guò)TypeScript的同學(xué),這讓人眼前一亮(從我的經(jīng)驗(yàn)看,國(guó)內(nèi)偶爾有大廠會(huì)用,小廠基本沒(méi)有)。隨后,我問(wèn)了句,你是怎么理解泛型的呢?問(wèn)了之后,我就后悔了,因?yàn)槲乙膊恢来鸢?。但隨后的答案讓我沒(méi)有后悔,因?yàn)楹蜻x人回了我一句,我不知道什么是泛型……
這件事對(duì)候選人的影響可大可小,但對(duì)我的影響挺大的。它致使我一定要寫(xiě)出一篇關(guān)于泛型的文章。但自從種下這個(gè)種子后,我就開(kāi)始后悔了。因?yàn)樵浇佑|TS中的泛型,越覺(jué)得這個(gè)題材沒(méi)什么好寫(xiě)的。一來(lái)呢,TS中的泛型猶如空氣,經(jīng)常使用卻難以描述。二者呢,它太過(guò)寬泛,難以面面俱到。
今天的這篇文章將不同于這個(gè)系列的以往。這篇文章將從C++模版要解決的問(wèn)題出發(fā),引出TS泛型要解決的問(wèn)題,并簡(jiǎn)答介紹一些稍微高級(jí)的使用場(chǎng)景。
一、模版
說(shuō)起泛型,不得不提一下泛型的鼻祖,模版。C++中的模版以燒腦殼和強(qiáng)大著稱,并被各類大牛津津樂(lè)道多年。就現(xiàn)在而言,Java、.NET或TS中的泛型都可以被認(rèn)為是實(shí)現(xiàn)了C++模版的子集。對(duì)于子集的說(shuō)法,我不敢茍同。因?yàn)榫痛嬖诘哪康亩?,TS和C++模版完全不一樣。
C++模版的出現(xiàn)是為了產(chǎn)生類型安全的通用容器。我們先來(lái)說(shuō)一下通用容器,比如我寫(xiě)了個(gè)鏈表或者數(shù)組,這個(gè)數(shù)據(jù)結(jié)構(gòu)不太關(guān)心存在里面的具體數(shù)據(jù)是什么類型,它都可以實(shí)現(xiàn)對(duì)應(yīng)的操作。但js本身不關(guān)注類型和大小,所以js中的數(shù)組本來(lái)就是通用容器。對(duì)于TS而言,泛型的出現(xiàn)就可以解決這個(gè)問(wèn)題。另一個(gè)值得對(duì)比的是產(chǎn)生,C++模版最終產(chǎn)出的是對(duì)應(yīng)的類或函數(shù),但對(duì)于TS而言,TS無(wú)法產(chǎn)生任何東西。有的同學(xué)可能要問(wèn)了,TS不是最終產(chǎn)生JS代碼嗎?這樣說(shuō)有點(diǎn)不嚴(yán)謹(jǐn),因?yàn)門(mén)S最終是分離出了JS代碼,而沒(méi)有對(duì)原有邏輯做任何處理。
C++模版的另一個(gè)目的就是元編程。這個(gè)元編程相當(dāng)?shù)貜?qiáng)大,它主要通過(guò)編譯時(shí)的程序設(shè)計(jì)構(gòu)造來(lái)優(yōu)化程序的執(zhí)行。就TS而言,目前它只做了一處類似的優(yōu)化,就是const enum可以內(nèi)聯(lián)在執(zhí)行的地方,僅此而已。關(guān)于這類優(yōu)化,上篇結(jié)束的位置也提到了基于類型推導(dǎo)的優(yōu)化,但目前而言,TS還沒(méi)有這個(gè)功能。倘若這類簡(jiǎn)單的優(yōu)化都不支持,那對(duì)于更為復(fù)雜的元編程而言,就更不可能了(元編程需要對(duì)泛型參數(shù)進(jìn)行邏輯推導(dǎo),并最終內(nèi)聯(lián)到使用到的地方)。
關(guān)于C++模版,就說(shuō)這么多吧,畢竟這不是一篇關(guān)于模版元編程的文章,而且我也不是專家,更多關(guān)于模版的問(wèn)題,可以去問(wèn)問(wèn)輪子哥。說(shuō)這么多模版,主要還是想說(shuō),TS中的泛型和模版是非常不一樣的!如果你是從C++或Java轉(zhuǎn)來(lái)做前端,仍然需要重新認(rèn)識(shí)一下TS中的泛型。
二、泛型
我認(rèn)為T(mén)S中的泛型主要有3個(gè)主要用途:
- 聲明泛型容器或組件。比如:各種容器類
Map、Array、Set等;各種組件,比如React.Component。 - 對(duì)類型進(jìn)行約束。比如:使用
extends約束傳入?yún)?shù)符合某種特定結(jié)構(gòu)。 - 生成新的類型
關(guān)于第二、三點(diǎn),因?yàn)橹拔恼乱呀?jīng)很清楚地提到過(guò),這里不再贅述。關(guān)于第一點(diǎn),我這里舉兩個(gè)例子:
第一個(gè)例子是關(guān)于泛型容器,假如我想實(shí)現(xiàn)一個(gè)簡(jiǎn)單的泛型鏈表,代碼如下:
class LinkedList<T> { // 泛型類
value: T;
next?: LinkedList<T>; // 可以使用自身進(jìn)行類型聲明
constructor(value: T, next?: LinkedList<T>) {
this.value = value;
this.next = next;
}
log() {
if (this.next) {
this.next.log();
}
console.log(this.value);
}
}
let list: LinkedList<number>; // 泛型特化為number
[1, 2, 3].forEach(value => {
list = new LinkedList(value, list);
});
list.log(); // 1 2 3
第二個(gè)是泛型組件,假如我想實(shí)現(xiàn)一個(gè)通用的表單組件,可以這樣寫(xiě):
function Form<T extends { [key: string]: any }>({ data }: { data: T }) {
return (
<form>
{data.map((value, key) => <input name={key} value={value} />)}
</form>
)
}
這個(gè)例子不止演示了泛型組件,也演示了如何使用extends定義泛型約束?,F(xiàn)實(shí)中的泛型表單組件可能比這個(gè)更為復(fù)雜,上面只是演示一下思路。
到此為止,TS的泛型就講完了!但這個(gè)文章還沒(méi)完,下面我們來(lái)看一下泛型的一些高級(jí)使用技巧。
三、泛型遞歸
遞歸簡(jiǎn)單來(lái)說(shuō)就是函數(shù)的輸出可以繼續(xù)作為輸入來(lái)進(jìn)行邏輯演算的一類解決問(wèn)題的思路。舉個(gè)簡(jiǎn)單的例子,比如我們要算加法,定義了一個(gè)add函數(shù),它只能求兩個(gè)數(shù)的和,但現(xiàn)在我們有1,2,3等三個(gè)數(shù)需要計(jì)算,那我們?nèi)绾斡矛F(xiàn)有的工具解決這個(gè)問(wèn)題呢?答案很簡(jiǎn)單,首先算add(1, 2)是3,然后add(3, 3)是6。這就是遞歸的思路。
在現(xiàn)實(shí)生活中,遞歸是如此的常見(jiàn),以至于我們經(jīng)常忽略它的存在。程序的世界也是如此。這里舉個(gè)例子,并用這個(gè)例子來(lái)說(shuō)明TS中的遞歸如何實(shí)現(xiàn)。比如,我現(xiàn)在有個(gè)泛型類型ReturnType<T>,它可以返回一個(gè)函數(shù)的返回類型。但我現(xiàn)在有個(gè)調(diào)用層級(jí)很深的函數(shù),而且我不知道它的層級(jí)有多深,我該如何做呢?
思路一:
type DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? DeepReturnType<ReturnType<T>> // 這里引用自身 : ReturnType<T>;
上面代碼的說(shuō)明:這里定義了一個(gè)DeepReturnType的泛型類型,類型約束為接受任意參數(shù)、返回任意類型的函數(shù)。若它的返回類型是個(gè)函數(shù),則繼續(xù)用返回類型調(diào)用自身,否則返回函數(shù)的返回類型。
任何直觀、簡(jiǎn)潔的方案背后都有一個(gè)但是。但是,這個(gè)是無(wú)法通過(guò)編譯的。主要原因是,TS暫時(shí)不支持。以后支不支持我不知道,但,官方給的理由很明確:
- 這個(gè)有著環(huán)形的意圖不可能構(gòu)成對(duì)象圖,除非你以某種方式推遲(通過(guò)惰性或狀態(tài))。
- 真的沒(méi)有辦法知道類型推導(dǎo)是否結(jié)束。
- 我們可以在編譯器中使用有限類型的遞歸,但問(wèn)題不在于類型是否終止,而是計(jì)算密集程度和內(nèi)存分配律如何。
- 一個(gè)元問(wèn)題:我們是否希望人們編寫(xiě)這樣的代碼?這種使用場(chǎng)景是存在的,但這樣實(shí)現(xiàn)的類型不一定適合庫(kù)的消費(fèi)者。
- 結(jié)論:我們還沒(méi)有為這種件事做好準(zhǔn)備。
所以,我們?cè)撊绾螌?shí)現(xiàn)這類需求呢?方法是有的,如官方給出的思路,我們可以使用有限次數(shù)的遞歸。下面給出我的思路:
// 兩層泛型類型 type ReturnType1<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? ReturnType<ReturnType<T>> : ReturnType<T>; // 三層泛型類型 type ReturnType2<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? ReturnType1<ReturnType<T>> : ReturnType<T>; // 四層泛型類型,可以滿足絕大多數(shù)情況 type DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? ReturnType2<ReturnType<T>> : ReturnType<T>; // 測(cè)試 const deep3Fn = () => () => () => () => "flag is win" as const; // 四層函數(shù) type Returned = DeepReturnType<typeof deep3Fn>; // type Returned = "flag is win" const deep1Fn = () => "flag is win" as const; // 一層函數(shù) type Returned = DeepReturnType<typeof deep1Fn>; // type Returned = "flag is win"
這種技巧可以推廣到定義深層結(jié)構(gòu)的Exclude、Optional或Required等等。
四、默認(rèn)泛型參數(shù)
有時(shí)候我們很喜歡泛型,但有時(shí)候我們又不希望類或函數(shù)的消費(fèi)者每次都指定泛型的類型,這時(shí)候,我們可以使用默認(rèn)的泛型參數(shù)。這個(gè)在很多第三方庫(kù)中廣泛使用,比如:
// 接收P S C的泛型組件
class Component<P,S,C> {
props: P;
state: S;
context:C
....
}
// 需要這樣使用
class MyComponent extends Component<{}, {}, {}>{}
// 但如果我的組件是個(gè)很純粹的組件,并不需要props、state和context呢
// 可以這樣定義
class Component<P = {}, S = {}, C = {}> {
props: P;
state: S;
context:C
....
}
// 然后可以這么使用
class MyComponent extends Component {}
我覺(jué)得這個(gè)特性非常實(shí)用,它以一種js中很自然的方式實(shí)現(xiàn)了C++模版中的partial instantiation。
五、泛型重載
泛型重載在官方文檔上提過(guò)幾嘴,這種重載依賴于函數(shù)重載的一些機(jī)制,因此,我們先來(lái)看一下TS中的函數(shù)重載吧。這里,我用lodash里面的map函數(shù)來(lái)舉例。map函數(shù)的第二個(gè)參數(shù)可以接受一個(gè)string或是function,比如官網(wǎng)的例子:
const square = (n) => n * n;
// 接收函數(shù)的map
map({ 'a': 4, 'b': 8 }, square);
// => [16, 64] (iteration order is not guaranteed)
const users = [
{ 'user': 'barney' },
{ 'user': 'fred' }
];
// 接收string的map
map(users, 'user');
// => ['barney', 'fred']
那么,這樣的類型聲明如何在TS中表達(dá)呢?我可以使用函數(shù)重載,比如這樣:
// 這里只做演示,不保證正確性。真實(shí)場(chǎng)景下這里需要填充正確的類型,而不是any
interface MapFn {
(obj: any, prop: string): any; // 當(dāng)接收string時(shí)的情況,情景一
(obj: any, fn: (value: any) => any): any; // 當(dāng)接收函數(shù)時(shí)的情況,情景二
}
const map: MapFn = () => ({});
map(users, 'user'); // 重載情景一
map({ 'a': 4, 'b': 8 }, square); // 重載情景二
上面這段代碼使用了TS中比較奇特的一種機(jī)制,也就是函數(shù)、new等 類函數(shù)的定義可以寫(xiě)在interface中。這個(gè)特性的出現(xiàn)主要是為了支持js中可調(diào)用的對(duì)象,比如,在jQuery中,我們可以直接執(zhí)行$("#banner-message"),或者調(diào)用其方法 $.ajax()。
當(dāng)然,也可以使用另一種更為傳統(tǒng)的做法,比如下面這樣:
function map(obj: any, prop: string): any;
function map(obj: any, fn: (value: any) => any): any;
function map(obj, secondary): any {}
這里,基本講清楚了函數(shù)重載。推廣到泛型,基本上是一樣的。這里舉一個(gè)知友提的問(wèn)題的例子,對(duì)于這個(gè)問(wèn)題,這里不再贅述。解決思路大概是這樣的:
interface FN {
(obj: { value: string; onChange: () => {} }): void;
<T extends {[P in keyof T]: never}>(obj: T): void;
// ,對(duì)于obj的類型T而言,它始終不接收其它的key。
}
const fn: FN = () => {};
fn({}); // 正確
fn({ value: "Hi" }); // 錯(cuò)誤
fn({ onChange: () => {} }); // 錯(cuò)誤
fn({ value: "Hi", onChange: () => ({}) }); // 正確
對(duì)于React生態(tài),這里有一個(gè)比較值得閱讀的泛型重載的實(shí)例,那就是connect函數(shù),大家可以移步到它的源碼以便了解更多。
整體而言,我不太喜歡這篇文章。究其原因,TS中的泛型使用廣泛,因其設(shè)計(jì)初衷的原因,可玩性較差。但我對(duì)這種設(shè)計(jì)理念是支持的,首先,它能夠滿足我們定義類型的要求,其次,它做到了比C++模版更為簡(jiǎn)單易用。
到此這篇關(guān)于關(guān)于C++ TpeScript系列的泛型的文章就介紹到這了,更多相關(guān)TypeScript泛型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
直觀詳細(xì)的typescript隱式類型轉(zhuǎn)換圖文詳解
這篇文章主要為大家介紹了直觀詳細(xì)的typescript隱式類型轉(zhuǎn)換圖文詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
JS前端html2canvas手寫(xiě)示例問(wèn)題剖析
這篇文章主要為大家介紹了JS前端html2canvas手寫(xiě)示例問(wèn)題剖析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
wasm+js實(shí)現(xiàn)文件獲取md5示例詳解
這篇文章主要為大家介紹了wasm+js實(shí)現(xiàn)md5文件獲取示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
JS實(shí)現(xiàn)可恢復(fù)的文件上傳示例詳解
這篇文章主要為大家介紹了JS實(shí)現(xiàn)可恢復(fù)的文件上傳示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
JavaScript?Canvas實(shí)現(xiàn)噪點(diǎn)濾鏡回憶童年電視雪花屏
這篇文章主要為大家介紹了JavaScript?Canvas實(shí)現(xiàn)噪點(diǎn)濾鏡回憶童年電視雪花屏,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
微信小程序promsie.all和promise順序執(zhí)行
這篇文章主要介紹了微信小程序promsie.all和promise順序執(zhí)行的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下2017-10-10

