關於C++ TpeScript系列的泛型
目錄
- 一、模版
- 二、泛型
- 三、泛型遞迴
- 四、預設泛型引數
- 五、泛型過載
前言:
我在面試的時候,通常喜歡問候選人一些莫名其妙的問題。比如這樣的問題,假如你是某個庫的作者,你如何實現某個功能。這類問題一般沒有正確的答案,主要意圖是考察一下候選人對這個庫有沒有更深入的理解,次要意圖是覺得這樣挺好玩。玩歸玩,但該嚴肅的時候也要嚴肅起來。有一次,我面試到一位用過TypeScript
的同學,這讓人眼前一亮(從我的經驗看,國內偶爾有大廠會用,小廠基本沒有)。隨後,我問了句,你是怎麼理解泛型的呢?問了之後,我就後悔了,因為我也不知道答案。但隨後的答案讓我沒有後悔,因為候選人回了我一句,我不知道什麼是泛型……
這件事對候選人的影響可大可小,但對我的影響挺大的。它致使我一定要寫出一篇關於泛型的文章。但自從種下這個種子後,我就開始後悔了。因為越接觸TS中的泛型,越覺得這個題材沒什麼好寫的。一來呢,TS中的泛型猶如空氣,經常使用卻難以描述。二者呢,它太過寬泛,難以面面俱到。
今天的這篇文章將不同於這個系列的以往。這篇文章將從C++模版要解決的問題出發,引出TS泛型要解決的問題,並簡答介紹一些稍微高階的使用場景。
一、模版
說起泛型,不得不提一下泛型的鼻祖,模版。C++中的模版以燒腦殼和強大著稱,並被各類大牛津津樂道多年。就現在而言,、.NET或TS中的泛型都可以被認為是實現了C++模版的子集。對於子集的說法,我不敢苟同。因為就存在的目的而言,TS和C++模版完全不一樣。
C++模版的出現是為了產生型別安全的通用容器。我們先來說一下通用容器,比如我寫了個連結串列或者陣列,這個資料結構不太關心存在裡面的具體資料是什麼型別,它都可以實現對應的操作。但本身不關注型別和大小,所以js中的陣列本來就是通用容器。對於TS而言,泛型的出現就可以解決這個問題。另一個值得對比的是產生,C++模版最終產出的是對應的類或函式,但對於TS而言,TS無法產生任何東西。有的同學可能要問了,TS不是最終產生JS程式碼嗎?這樣說有點不嚴謹,因為TS最終是分離出了JS程式碼,而沒有對原有邏輯做任何處理。
C++模版的另一個目的就是元。這個超程式設計相當地強大,它主要通過編譯時的程式設計構造來優化程式的執行。就TS而言,目前它只做了一處類似的優化,就是const enum可以內聯在執行的地方,僅此而已。關於這類優化,上篇結束的位置也提到了基於型別推導的優化,但目前而言,TS還沒有這個功能。倘若這類簡單的優化都不支援,那對於更為複雜的超程式設計而言,就更不可能了(超程式設計需要對泛型引數進行邏輯推導,並最終內聯到使用到的地方)。
關於C++模版,就說這麼多吧,畢竟這不是一篇關於模版超程式設計的文章,而且我也不是專家,更多關於模版的問題,可以去問問輪子哥。說這麼多模版,主要還是想說,TS中的泛型和模版是非常不一樣的!如果你是從C++
Java
轉來做前端,仍然需要重新認識一下TS中的泛型。
二、泛型
我認為TS中的泛型主要有3個主要用途:
- 宣告泛型容器或元件。比如:各種容器類
Map
、Array
、Set等;各種元件,比如React.Component
。 - 對型別進行約束。比如:使用
extends
約束傳入引數符合某種特定結構。 - 生成新的型別
關於第二、三點,因為之前文章已經很清楚地提到過,這裡不再贅述。關於第一點,我這裡舉兩個例子:
第一個例子是關於泛型容器,假如我想實現一個簡單的泛型連結串列,程式碼如下:
class LinkedList<T> { // 泛型類
value: T;
next?: LinkedList<T>; // 可以使用自身進行型別宣告
constructor(value: T,next?: LinkedList<T>) {
this.value = value;
this.next = next;
}
log() {
if (this.next) {
this.next.log();
}
cohttp://www.cppcns.comnsole.log(this.value);
}
}
let list: LinkedList<number>; // 泛型特化為number
[1,2,3].forEach(value => {
list = new LinkedList(value,list);
});
list.log(); // 1 2 3
第二個是泛型元件,假如我想實現一個通用的表單元件,可以這樣寫:
function Form<T extends { [key: string]: any }>({ data }: { data: T }) { return ( <form> {data.map((value,key) => <input name={key} value={value} />)} </form> ) }
這個例子不止演示了泛型元件,也演示瞭如何使用extends定義泛型約束。現實中的泛型表單元件可能比這個更為複雜,上面只是演示一下思路。
到此為止,TS的泛型就講完了!但這個文章還沒完,下面我們來看一下泛型的一些高階使用技巧。
三、泛型遞迴
遞迴簡單來說就是函式的輸出可以繼續作為輸入來進行邏輯演算的一類解決問題的思路。舉個簡單的例子,比如我們要算加法,定義了一個add
函式,它只能求兩個數的和,但現在我們有1,2,3等三個數需要計算,那我們如何用現有的工具解決這個問題呢?答案很簡單,首先算add(1,2)是3,然後add(3,3)是6。這就是遞迴的思路。
在現實生活中,遞迴是如此的常見,以至於我們經常忽略它的存在。程式的世界也是如此。這裡舉個例子,並用這個例子來說明TS中的遞迴如何實現。比如,我現在有個泛型型別ReturnType<T>,它可以返回一個函式的返回型別。但我現在有個呼叫層級很深的函式,而且我不知道它的層級有多深,我該如何做呢?
思路一:
type DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? DeepReturnType<ReturnType<T>> // 這裡引用自身 : ReturnType<T>;
上面程式碼的說明:這裡定義了一個DeepReturnType
的泛型型別,型別約束為接受任意引數、www.cppcns.com返回任意型別的函式。若它的返回型別是個函式,則繼續用返回型別呼叫自身,否則返回函式的返回型別。
任何直觀、簡潔的方案背後都有一個但是。但是,這個是無法通過編譯的。主要原因是,TS暫時不支援。以後支不支援我不知道,但,官方給的理由很明確:
- 這個有著環形的意圖不可能構成物件圖,除非你以某種方式推遲(通過惰性或狀態)。
- 真的沒有辦法知道型別推導是否結束。
- 我們可以在編譯器中使用有限型別的遞迴,但問題不在於型別是否終止,而是計算密集程度和記憶體分配律如何。
- 一個元問題:我們是否希望人們編寫這樣的程式碼?這種使用場景是存在的,但這樣實現的型別不一定適合庫的消費者。
- 結論:我們還沒有為這種件事做好準備。
所以,我們該如何實現這類需求呢?方法是有的,如官方給出的思路,我們可以使用有限次數的遞迴。下面給出我的思路:
// 兩層泛型型別 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>; // 四層泛型型別,可以滿足絕大多數情況 type DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? ReturnType2<ReturnType<T>> : ReturnType<T>; // 測試 const deep3Fn = () => () => () => () => "flag is win" as const; // 四層函式 type Returned = DeepReturnType<typeof deep3Fn>; // type Returned = "flag is win" const deep1Fn = () => "flag is win" as const; // 一層函式 type Returned = DeepReturnType<typeof deep1Fn>; // type Returned = "flag is win"
這種技巧可以推廣到定義深層結構的Exclude
、Optional
或Required
等等。
四、預設泛型引數
有時候我們很喜歡泛型,但有時候我們又不希望類或函式的消費者每次都指定泛型的型別,這時候,我們可以使用預設的泛型引數。這個在很多第三方庫中廣泛使用,比如:
// 接收P S C的泛型元件 class Component<P,S,C> { props: P; state: S; context:C .... } // 需要這樣使用 class MyComponent extends Component<{},{},{}>{} // 但如果我的元件是個很純粹的元件,並不需要props、state和context呢 // 可以這樣定義 class Component<P = {},S = {},C = {}> { props: P; state: S; context:C .... } // 然後可以這麼使用 class MyComponent extends Component {}
我覺得這個特性非常實用,它以一種js中很自然的方式實現了C++模版中的partial instantiation
。
五、泛型過載
泛型過載在官方文件上提過幾嘴,這種過載依賴於函式過載的一些機制,因此,我們先來看一下TS中的函式過載吧。這裡,我用lodash
裡面的map
函式來舉例。map函式的第二個引數可以接受一個string
或是function
,比如官網的例子:
const square = (n) => n * n; // 接收函式的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中表達呢?我可以使用函式過載,比如這樣:
// 這裡只做演示,不保證正確性。真實場景下這裡需要填充正確的型別,而不是any interface MapFn { (obj: any,prop: string): any; // 當接收string時的情況,情景一 (obj: any,fn: (value: any) => any): any; // 當接收函式時的情況,情景二 } const map: MapFn = () => ({}); map(users,'user'); // 過載情景一 map({ 'a': 4,square); // 過載情景二
上面這段程式碼使用了TS中比較奇特的一種機制,也就是函式、new等 類函式的定義可以寫在interface
中。這個特性的出現主要是為了支援js中可呼叫的物件,比如,在中,我們可以直接執行
$("#banner-message"),
或者呼叫其方法 $.ajax()。
當然,也可以使用另一種更為傳統的做法,比如下面這樣:
function map(ohttp://www.cppcns.combj: any,prop: string): any; function map(obj: any,fn: (value: any) => any): any; function map(obj,secondary): any {}
這裡,基本講清楚了函式過載。推廣到泛型,基本上是一樣的。這裡舉一個知友提的問題的例子,對於這個問題,這裡不再贅述。解決思路大概是這樣的:
interface FN { (obj: { value: string; onChange: () => {} }): void; <T extends {[P in keyof T]: never}>(obj: T): void; // ,對於obj的型別T而言,它始終不接收其它的key。 } const fn: FN = () =>uETzQihfj; {}; fn({}); // 正確 fn({ value: "Hi" }); // 錯誤 fn({ onChange: () => {} }); // 錯誤 fn({ value: "Hi",onChange: () => ({}) }); // 正確
對於React生態,這裡有一個比較值得閱讀的泛型過載的例項,那就是connect
函式,大家可以移步到它的原始碼以便了解更多。
整體而言,我不太喜歡這篇文章。究其原因,TS中的泛型使用廣泛,因其設計初衷的原因,可玩性較差。但我對這種設計理念是支援的,首先,它能夠滿足我們定義型別的要求,其次,它做到了比C++模版更為簡單易用。
到此這篇關於關於C++ TpeScript系列的泛型的文章就介紹到這了,更多相關TypeScript泛型內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!