你不知道的 TypeScript 泛型(萬字長文,建議收藏)

泛型是 TypeScript(如下簡稱 TS) 比較高級的功能之一,理解起來也比較困難。泛型應用場景很是普遍,不少地方都能看到它的影子。平時咱們閱讀開源 TS 項目源碼,或者在本身的 TS 項目中使用一些第三方庫(好比 React)的時候,常常會看到各類泛型定義。若是你不是特別瞭解泛型,那麼你極可能不只不會用,不會實現,甚至看不懂這是在幹什麼。html

相信你們都經歷過,看到過,或者正在寫一些應用,這些應用充斥着各類重複類型定義, any 類型層出不窮,鼠標移到變量上面的提示只有 any,不要說類型操做了,類型能寫對都是個問題。我也經歷過這樣的階段,那個時候我對 TS 還比較陌生。前端

隨着在 TS 方面學習的深刻,愈來愈認識到 真正的 TS 高手都是在玩類型,對類型進行各類運算生成新的類型。這也好理解,畢竟 TS 提供的其實就是類型系統。你去看那些 TS 高手的代碼,會各類花式使用泛型。 能夠說泛型是一道坎,只有真正掌握它,你才知道原來 TS 還能夠這麼玩。怪不得面試的時候你們都願意問泛型,儘管面試官極可能也不怎麼懂。react

只有理解事物的內在邏輯,纔算真正掌握了,否則永遠只是皮毛,不得其法。 本文就帶你走進泛型,帶你從另外一個角度看看究竟什麼是泛型,爲何要有它,它給 TS 帶來了什麼樣的不一樣。git

注意:不一樣語言泛型略有不一樣,知識遷移雖然能夠,可是不能生搬硬套,本文所講的泛型都指的是 TS 下的泛型。程序員

引言

我總結了一下,學習 TS 有兩個難點。第一個是TS 和 JS 中容易混淆的寫法,第二個是TS中特有的一些東西github

  • TS 中容易引發你們的混淆的寫法

好比:web

容易混淆的箭頭函數
容易混淆的箭頭函數

(容易混淆的箭頭函數)面試

再好比:typescript

(容易混淆的 interface 內的小括號)編程

  • TS 中特有的一些東西

好比 typeof,keyof, infer 以及本文要講的泛型。

把這些和 JS 中容易混淆的東西分清楚,而後搞懂 TS 特有的東西,尤爲是泛型(其餘基本上相對簡單),TS 就入門了。

泛型初體驗

在強類型語言中,通常而言須要給變量指定類型才能使用該變量。以下代碼:

const name: string = "lucifer";
console.log(name); 複製代碼

咱們須要給 name 聲明 string 類型,而後才能在後面使用 name 變量,當咱們執行如下操做的時候會報錯。

  • 給 name 賦其餘類型的值
  • 使用其餘類型值特有的方法(好比 Number 類型特有的 toFixed)
  • 將 name 以參數傳給不支持 string 的函數。 好比 divide(1, name),其中 divide 就是功能就是 將第一個數(number 類型)除以第二個數(number 類型),並將結果返回

TS 除了提供一些基本類型(好比上面的 string)供咱們直接使用。還:

  • 提供了 intefacetype 關鍵字供咱們定義本身的類型,以後就能像使用基本類型同樣使用本身定義的類型了。
  • 提供了各類邏輯運算符,好比 &, | 等 ,供咱們對類型進行操做,從而生成新的類型。
  • 提供泛型,容許咱們在定義的時候不具體指定類型,而是泛泛地說一種類型,並在函數調用的時候再指定具體的參數類型。
  • 。。。

也就是說泛型也是一種類型,只不過不一樣於 string, number 等具體的類型,它是一種抽象的類型,咱們不能直接定義一個變量類型爲泛型。

簡單來講,區別於平時咱們對進行編程,泛型是對類型進行編程。這個聽起來比較抽象。以後咱們會經過若干實例帶你理解這句話,你先留一個印象就好。

爲了明白上面這句話,·首先要區分「值」和「類型」。

值和類型

咱們平時寫代碼基本都是對值編程。好比:

if (person.isVIP) {
 console.log('VIP') } if (cnt > 5) {  // do something }  const personNames = persons.map(p => p.name) ... 複製代碼

能夠看出這都是對具體的值進行編程,這符合咱們對現實世界的抽象。從集合論的角度上來講, 值的集合就是類型,在 TS 中最簡單的用法是對值限定類型,從根本上來講是限定值的集合。這個集合能夠是一個具體的集合,也能夠是多個集合經過集合運算(交叉並)生成的新集合。

(值和類型)

再來看一個更具體的例子:

function t(name: string) {
 return `hello, ${name}`; } t("lucifer"); 複製代碼

字符串 "lucifer" 是 string 類型的一個具體。 在這裏 "lucifer" 就是值,而 string 就是類型。

TS 明白 "lucifer" 是 string 集合中的一個元素,所以上面代碼不會有問題,可是若是是這樣就會報錯:

t(123);
複製代碼

由於 123 並非 string 集合中的一個元素。

對於 t("lucifer")而言,TS 判斷邏輯的僞代碼:

v = getValue(); // will return 'lucifer' by ast
if (typeof v === "string") {  // ok } else {  throw "type error"; } 複製代碼

因爲是靜態類型分析工具,所以 TS 並不會執行 JS 代碼,但並非說 TS 內部沒有執行邏輯。

簡單來總結一下就是: 值的集合就是類型,平時寫代碼基本都是對值編程,TS 提供了不少類型(也能夠自定義)以及不少類型操做幫助咱們限定值以及對值的操做

什麼是泛型

上面已經鋪墊了一番,你們已經知道了值和類型的區別,以及 TS 究竟幫咱們作了什麼事情。可是直接理解泛型仍然會比較吃力,接下來我會經過若干實例,慢慢帶你們走進泛型。

首先來思考一個問題:爲何要有泛型呢?這個緣由實際上有不少,在這裏我選擇你們廣泛認同的一個切入點來解釋。若是你明白了這個點,其餘點相對而言理解起來會比較輕鬆。仍是經過一個例子來進行說明。

不容小覷的 id 函數

假如讓你實現一個函數 id,函數的參數能夠是任何值,返回值就是將參數原樣返回,而且其只能接受一個參數,你會怎麼作?

你會以爲這很簡單,順手就寫出這樣的代碼:

const id = (arg) => arg;
複製代碼

有的人可能以爲 id 函數沒有什麼實際做用。其實否則, id 函數在函數式編程中應用很是普遍。

因爲其能夠接受任意值,也就是說你的函數的入參和返回值都應該能夠是任意類型。 如今讓咱們給代碼增長類型聲明:

type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number; type idString = (arg: string) => string; ... 複製代碼

一個笨的方法就像上面那樣,也就是說 JS 提供多少種類型,就須要複製多少份代碼,而後改下類型簽名。這對程序員來講是致命的。這種複製粘貼增長了出錯的機率,使得代碼難以維護,牽一髮而動全身。而且未來 JS 新增新的類型,你仍然須要修改代碼,也就是說你的代碼對修改開放,這樣很差。還有一種方式是使用 any 這種「萬能語法」。缺點是什麼呢?我舉個例子:

id("string").length; // ok
id("string").toFixed(2); // ok id(null).toString(); // ok ... 複製代碼

若是你使用 any 的話,怎麼寫都是 ok 的, 這就喪失了類型檢查的效果。實際上我知道我傳給你的是 string,返回來的也必定是 string,而 string 上沒有 toFixed 方法,所以須要報錯纔是我想要的。也就是說我真正想要的效果是:當我用到id的時候,你根據我傳給你的類型進行推導。好比我傳入的是 string,可是使用了 number 上的方法,你就應該報錯。

爲了解決上面的這些問題,咱們使用泛型對上面的代碼進行重構。和咱們的定義不一樣,這裏用了一個 類型 T,這個 T 是一個抽象類型,只有在調用的時候才肯定它的值,這就不用咱們複製粘貼無數份代碼了。

function id<T>(arg: T): T {
 return arg; } 複製代碼

爲何這樣就能夠了? 爲何要用這種寫法?這個尖括號什麼鬼?萬物必有因果,之因此這麼設計泛型也是有緣由的。那麼就讓我來給你們解釋一下,相信不少人都沒有從這個角度思考過這個問題。

泛型就是對類型編程

上面提到了一個重要的點 平時咱們都是對值進行編程,泛型是對類型進行編程。上面我沒有給你們解釋這句話。如今鋪墊足夠了,那就讓咱們開始吧!

繼續舉一個例子:假如咱們定義了一個 Person 類,這個 Person 類有三個屬性,而且都是必填的。這個 Person 類會被用於用戶提交表單的時候限定表單數據。

enum Sex {
 Man,  Woman,  UnKnow, } interface Person {  name: string;  sex: Sex;  age: number; } 複製代碼

忽然有一天,公司運營想搞一個促銷活動,也須要用到 Person 這個 shape,可是這三個屬性均可以選填,同時要求用戶必須填寫手機號以便標記用戶和接受短信。一個很笨的方法是從新寫一個新的類:

interface MarketPerson {
 name?: string;  sex?: Sex;  age?: number;  phone: string; } 複製代碼

還記得我開頭講的重複類型定義麼? 這就是!

這明顯不夠優雅。若是 Person 字段不少呢?這種重複代碼會異常多,不利於維護。 TS 的設計者固然不容許這麼醜陋的設計存在。那麼是否能夠根據已有類型,生成新的類型呢?固然能夠!答案就是前面我提到了兩種對類型的操做:一種是集合操做,另外一種是今天要講的泛型。

先來看下集合操做:

type MarketPerson = Person & { phone: string };
複製代碼

這個時候咱們雖然添加了一個必填字段 phone,可是沒有作到name, sex, age 選填,彷佛集合操做作不到這一點呀。咱們腦洞一下,假如咱們能夠像操做函數那樣操做類型,是否是有可能呢?好比我定義了一個函數 Partial,這個函數的功能入參是一個類型,返回值是新的類型,這個類型裏的屬性所有變成可選的。

僞代碼:

function Partial(Type) {  type ans = 空類型  for(k in Type) {  空類型[k] = makeOptional(Type, k)  }  return ans }  type PartialedPerson = Partial(Person)  複製代碼

惋惜的是上面代碼不能運行,也不可能運行。不可能運行的緣由有:

  • 這裏使用函數 Partial 操做類型,能夠看出上面的函數我是沒有添加簽名的,我是故意的。若是讓你給這個函數添加簽名你怎麼加?沒辦法加!
  • 這裏使用 JS 的語法對類型進行操做,這是不恰當的。首先這種操做依賴了 JS 運行時,而 TS 是靜態分析工具,不該該依賴 JS 運行時。其次若是要支持這種操做是否意味者 TS 對 JS 妥協,JS 出了新的語法(好比早幾年出的 async await),TS 都要支持其對 TS 進行操做。

所以迫切須要一種不依賴 JS 行爲,特別是運行時行爲的方式,而且邏輯其實和上面相似的,且不會和現有語法體系衝突的語法。 咱們看下 TS 團隊是怎麼作的:

// 能夠當作是上面的函數定義,能夠接受任意類型。因爲是這裏的 「Type」 形參,所以理論上你叫什麼名字都是無所謂的,就好像函數定義的形參同樣。
type Partial<Type> = { do something } // 能夠當作是上面的函數調用,調用的時候傳入了具體的類型 Person type PartialedPerson = Partial<Person> 複製代碼

先無論功能,咱們來看下這兩種寫法有多像:

(定義)

(運行)

再來看下上面泛型的功能。上面代碼的意思是對 T 進行處理,是返回一個 T 的子集,具體來講就是將 T 的全部屬性變成可選。這時 PartialedPerson 就等於 :

interface Person {
 name?: string;  sex?: Sex;  age?: number; } 複製代碼

功能和上面新建一個新的 interface 同樣,可是更優雅。

最後來看下泛型 Partial 的具體實現,能夠看出其沒有直接使用 JS 的語法,而是本身定義了一套語法,好比這裏的 keyof,至此徹底應證了我上面的觀點。

type Partial<T> = { [P in keyof T]?: T[P] };
複製代碼

剛纔說了「因爲是形參,所以起什麼名字無所謂」 。所以這裏就起了 T 而不是 Type,更短了。這也算是一種約定俗稱的規範,你們通常習慣叫 T, U 等表示泛型的形參。

咱們來看下完整的泛型和函數有多像!

(定義)

(使用)

  • 從外表看只不過是 function 變成了 type() 變成了 <>而已。

  • 從語法規則上來看, 函數內部對標的是 ES 標準。而泛型對應的是 TS 實現的一套標準。

簡單來講,將類型當作值,而後對類型進行編程,這就是泛型的基本思想。泛型相似咱們平時使用的函數,只不過其是做用在類型上,思想上和咱們平時使用的函數並無什麼太多不一樣,泛型產生的具體類型也支持類型的操做。好比:

type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
複製代碼

有了上面的知識,咱們經過幾個例子來鞏固一下。

function id<T, U>(arg1: T, arg2: U): T {
 return arg1; } 複製代碼

上面定義了泛型 id,其入參分別是 T 和 U,和函數參數同樣,使用逗號分隔。定義了形參就能夠在函數體內使用形參了。如上咱們在函數的參數列表和返回值中使用了形參 T 和 U。

返回值也能夠是複雜類型:

function ids<T, U>(arg1: T, arg2: U): [T, U] {
 return [arg1, arg2]; } 複製代碼

(泛型的形參)

和上面相似, 只不過返回值變成了數組而已。

須要注意的是,思想上咱們能夠這樣去理解。可是具體的實現過程會有一些細微差異,好比:

type P = [number, string, boolean];
type Q = Date;  type R = [Q, ...P]; // A rest element type must be an array type. 複製代碼

再好比:

type Lucifer = LeetCode;
type LeetCode<T = {}> = {  name: T; };  const a: LeetCode<string>; //ok const a: Lucifer<string>; // Type 'Lucifer' is not generic. 複製代碼

改爲這樣是 ok 的:

type Lucifer<T> = LeetCode<T>;
複製代碼

泛型爲何使用尖括號

爲何泛型要用尖括號(<>),而不是別的? 我猜是由於它和 () 長得最像,且在如今的 JS 中不會有語法歧義。可是,它和 JSX 不兼容!好比:

function Form() { // ... 複製代碼return ( <Select<string> options={targets} value={target} onChange={setTarget} /> ); } 複製代碼

這是由於 TS 發明這個語法的時候,還沒想過有 JSX 這種東西。後來 TS 團隊在 TypeScript 2.9 版本修復了這個問題。也就是說如今你能夠直接在 TS 中使用帶有泛型參數的 JSX 啦(好比上面的代碼)。

泛型的種類

實際上除了上面講到的函數泛型,還有接口泛型和類泛型。不過語法和含義基本同函數泛型同樣:

interface id<T, U> {
 id1: T;  id2: U; } 複製代碼

(接口泛型)

class MyComponent extends React.Component<Props, State> {
 ... } 複製代碼

(類泛型)

總結下就是: 泛型的寫法就是在標誌符後面添加尖括號(<>),而後在尖括號裏寫形參,並在 body(函數體, 接口體或類體) 裏用這些形參作一些邏輯處理。

泛型的參數類型 - 「泛型約束」

正如文章開頭那樣,咱們能夠對函數的參數進行限定。

function t(name: string) {
 return `hello, ${name}`; } t("lucifer"); 複製代碼

如上代碼對函數的形參進行了類型限定,使得函數僅能夠接受 string 類型的值。那麼泛型如何達到相似的效果呢?

type MyType = (T: constrain) => { do something };
複製代碼

仍是以 id 函數爲例,咱們給 id 函數增長功能,使其不只能夠返回參數,還會打印出參數。熟悉函數式編程的人可能知道了,這就是 trace 函數,用於調試程序。

function trace<T>(arg: T): T {
 console.log(arg);  return arg; } 複製代碼

假如我想打印出參數的 size 屬性呢?若是徹底不進行約束 TS 是會報錯的:

注意:不一樣 TS 版本可能提示信息不徹底一致,個人版本是 3.9.5。下文的全部測試結果均是使用該版本,再也不贅述。

function trace<T>(arg: T): T {
 console.log(arg.size); // Error: Property 'size doesn't exist on type 'T'  return arg; } 複製代碼

報錯的緣由在於 T 理論上是能夠是任何類型的,不一樣於 any,你無論使用它的什麼屬性或者方法都會報錯(除非這個屬性和方法是全部集合共有的)。那麼直觀的想法是限定傳給 trace 函數的參數類型應該有 size 類型,這樣就不會報錯了。如何去表達這個類型約束的點呢?實現這個需求的關鍵在於使用類型約束。 使用 extends 關鍵字能夠作到這一點。簡單來講就是你定義一個類型,而後讓 T 實現這個接口便可。

interface Sizeable {
 size: number; } function trace<T extends Sizeable>(arg: T): T {  console.log(arg.size);  return arg; }  複製代碼

這個時候 T 就再也不是任意類型,而是被實現接口的 shape,固然你也能夠繼承多個接口。類型約束是很是常見的操做,你們必定要掌握。

有的人可能說我直接將 Trace 的參數限定爲 Sizeable 類型能夠麼?若是你這麼作,會有類型丟失的風險,詳情能夠參考這篇文章A use case for TypeScript Generics[1]

常見的泛型

集合類

你們平時寫 TS 必定見過相似 Array<String> 這種寫法吧? 這實際上是集合類,也是一種泛型。

本質上數組就是一系列值的集合,這些值能夠能夠是任意類型,數組只是一個容器而已。然而平時開發的時候一般數組的項目類型都是相同的,若是不加約束的話會有不少問題。 好比我應該是一個字符串數組,然是卻不當心用到了 number 的方法,這個時候類型系統應該幫我識別出這種類型問題

因爲數組理論能夠存聽任意類型,所以須要使用者動態決定你想存儲的數據類型,而且這些類型只有在被調用的時候才能去肯定。 Array<String> 就是調用,通過這個調用會產生一個具體集合,這個集合只能存放 string 類型的值。

不調用直接把 Array 是不被容許的:

const a: Array = ["1"];
複製代碼

如上代碼會被錯:Generic type 'Array<T>' requires 1 type argument(s).ts 。 有沒有以爲和函數調用沒傳遞參數報錯很像?像就對了。

這個時候你再去看 Set, Promise,是否是很快就知道啥意思了?它們本質上都是包裝類型,而且支持多種參數類型,所以能夠用泛型來約束。

React.FC

你們若是開發過 React 的 TS 應用,必定知道 React.FC 這個類型。咱們來看下它是如何定義[2]的:

type FC<P = {}> = FunctionComponent<P>;
 interface FunctionComponent<P = {}> {  (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;  propTypes?: WeakValidationMap<P>;  contextTypes?: ValidationMap<any>;  defaultProps?: Partial<P>;  displayName?: string; } 複製代碼

能夠看出其大量使用了泛型。你若是不懂泛型怎麼看得懂呢?無論它多複雜,咱們從頭一點點分析就行,記住我剛纔講的類比方法,將泛型類比到函數進行理解。·

  • 首先定義了一個泛型類型 FC,這個 FC 就是咱們平時用的 React.FC。它是經過另一個泛型 FunctionComponent 產生的。

所以,實際上第一行代碼的做用就是起了一個別名

  • FunctionComponent 其實是就是一個接口泛型,它定義了五個屬性,其中四個是可選的,而且是靜態類屬性。
  • displayName 比較簡單,而 propTypes,contextTypes,defaultProps 又是經過其餘泛型生成的類型。咱們仍然能夠採用個人這個分析方法繼續分析。因爲篇幅緣由,這裏就不一一分析,讀者能夠看完個人分析過程以後,本身嘗試分析一波。
  • (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null; 的含義是 FunctionComponent 是一個函數,接受兩個參數(props 和 context )返回 ReactElement 或者 null。ReactElement 你們應該比較熟悉了。 PropsWithChildren 實際上就是往 props 中插入 children,源碼也很簡單,代碼以下:
type PropsWithChildren<P> = P & { children?: ReactNode };
複製代碼

這不就是咱們上面講的集合操做可選屬性麼?至此,React.FC 的全貌咱們已經清楚了。讀者能夠試着分析別的源碼檢測下本身的學習效果,好比 React.useState 類型的簽名。

類型推導與默認參數

類型推導和默認參數是 TS 兩個重要功能,其依然能夠做用到泛型上,咱們來看下。

類型推導

咱們通常常見的類型推導是這樣的:

const a = "lucifer"; // 咱們沒有給 a 聲明類型, a 被推導爲 string
a.toFixed(); // Property 'toFixed' does not exist on type 'string'. a.includes("1"); // ok 複製代碼

須要注意的是,類型推導是僅僅在初始化的時候進行推導,以下是沒法正確推導的:

let a = "lucifer"; // 咱們沒有給 a 聲明類型, a 被推導爲string
a.toFixed(); // Property 'toFixed' does not exist on type 'string'. a.includes("1"); // ok a = 1; a.toFixed(); // 依然報錯, a 不會被推導 爲 number 複製代碼

而泛型也支持類型推導,以上面的 id 函數爲例:

function id<T>(arg: T): T {
 return arg; } id<string>("lucifer"); // 這是ok的,也是最完整的寫法 id("lucifer"); // 基於類型推導,咱們能夠這樣簡寫 複製代碼

這也就是爲何 useState 有以下兩種寫法的緣由。

const [name, setName] = useState("lucifer");
const [name, setName] = useState<string>("lucifer"); 複製代碼

實際的類型推導要更加複雜和智能。相信隨着時間的推動,TS 的類型推導會更加智能。

默認參數

類型推導相同的點是,默認參數也能夠減小代碼量,讓你少些代碼。前提是你要懂,否則伴隨你的永遠是大大的問號。其實你徹底能夠將其類比到函數的默認參數來理解。

舉個例子:

type A<T = string> = Array<T>;
const aa: A = [1]; // type 'number' is not assignable to type 'string'. const bb: A = ["1"]; // ok const cc: A<number> = [1]; // ok 複製代碼

上面的 A 類型默認是 string 類型的數組。你能夠不指定,等價於 Array,固然你也能夠顯式指定數組類型。有一點須要注意:在 JS 中,函數也是值的一種,所以:

const fn = () => null; // ok
複製代碼

可是泛型這樣是不行的,這是和函數不同的地方(設計缺陷?Maybe):

type A = Array; // error: Generic type 'Array<T>' requires 1 type argument(s).
複製代碼

其緣由在與 Array 的定義是:

interface Array<T> {
 ... } 複製代碼

而若是 Array 的類型也支持默認參數的話,好比:

interface Array<T = string> {
 ... } 複製代碼

那麼 type A = Array; 就是成立的,若是不指定的話,會默認爲 string 類型。

何時用泛型

若是你認真看完本文,相信應該知道何時使用泛型了,我這裏簡單總結一下。

當你的函數,接口或者類:

  • 須要做用到不少類型的時候,好比咱們介紹的 id 函數的泛型聲明。
  • 須要被用到不少地方的時候,好比咱們介紹的 Partial 泛型。

進階

上面說了泛型和普通的函數有着不少類似的地方。普通的函數能夠嵌套其餘函數,甚至嵌套本身從而造成遞歸。泛型也是同樣!

泛型支持函數嵌套

好比:

type CutTail<Tuple extends any[]> = Reverse<CutHead<Reverse<Tuple>>>;
複製代碼

如上代碼中, Reverse 是將參數列表反轉,CutHead 是將數組第一項切掉。所以 CutTail 的意思就是將傳遞進來的參數列表反轉,切掉第一個參數,而後反轉回來。換句話說就是切掉參數列表的最後一項。 好比,一個函數是 function fn (a: string, b: number, c: boolean):boolean {},那麼通過操做type cutTailFn = CutTail<typeof fn>,能夠返回(a: string, b:number) => boolean。 具體實現能夠參考Typescript 複雜泛型實踐:如何切掉函數參數表的最後一個參數?[3]。 在這裏,你知道泛型支持嵌套就夠了。

泛型支持遞歸

泛型甚至能夠嵌套本身從而造成遞歸,好比咱們最熟悉的單鏈表的定義就是遞歸的。

type ListNode<T> = {
 data: T;  next: ListNode<T> | null; }; 複製代碼

(單鏈表)

再好比 HTMLElement 的定義。

declare var HTMLElement: {
 prototype: HTMLElement;  new(): HTMLElement; };。 複製代碼

HTMLElement[4]

上面是遞歸聲明,咱們再來看一個更復雜一點的遞歸形式 - 遞歸調用,這個遞歸調用的功能是:遞歸地將類型中全部的屬性都變成可選。相似於深拷貝那樣,只不過這不是拷貝操做,而是變成可選,而且是做用在類型,而不是值。

type DeepPartial<T> = T extends Function
 ? T  : T extends object  ? { [P in keyof T]?: DeepPartial<T[P]> }  : T;  type PartialedWindow = DeepPartial<Window>; // 如今window 上全部屬性都變成了可選啦 複製代碼

TS 泛型工具及實現

雖然泛型支持函數的嵌套,甚至遞歸,可是其語法能力確定和 JS 無法比, 想要實現一個泛型功能真的不是一件容易的事情。這裏提供幾個例子,看完這幾個例子,相信你至少能夠達到比葫蘆畫瓢的水平。這樣多看多練,慢慢水平就上來了。

截止目前(2020-06-21),TS 提供了 16 種工具類型[5]

(官方提供的工具類型)

除了官方的工具類型,還有一些社區的工具類型,好比type-fest[6],你能夠直接用或者去看看源碼看看高手是怎麼玩類型的。

我挑選幾個工具類,給你們講一下實現原理

Partial

功能是將類型的屬性變成可選。注意這是淺 Partial,DeepPartial 上面我講過了,只要配合遞歸調用使用便可。

type Partial<T> = { [P in keyof T]?: T[P] };
複製代碼

Required

功能和Partial 相反,是將類型的屬性變成必填, 這裏的 -指的是去除。 -? 意思就是去除可選,也就是必填啦。

type Required<T> = { [P in keyof T]-?: T[P] };
複製代碼

Mutable

功能是將類型的屬性變成可修改,這裏的 -指的是去除。 -readonly 意思就是去除只讀,也就是可修改啦。

type Mutable<T> = {
 -readonly [P in keyof T]: T[P]; }; 複製代碼

Readonly

功能和Mutable 相反,功能是將類型的屬性變成只讀, 在屬性前面增長 readonly 意思會將其變成只讀。

type Readonly<T> = { readonly [P in keyof T]: T[P] };
複製代碼

ReturnType

功能是用來獲得一個函數的返回值類型。

type ReturnType<T extends (...args: any[]) => any> = T extends (
 ...args: any[] ) => infer R  ? R  : any; 複製代碼

下面的示例用 ReturnType 獲取到 Func 的返回值類型爲 string,因此,foo 也就只能被賦值爲字符串了。

type Func = (value: number) => string;
 const foo: ReturnType<Func> = "1"; 複製代碼

更多參考TS - es5.d.ts[7] 這些泛型能夠極大減小你們的冗餘代碼,你們能夠在本身的項目中自定義一些工具類泛型。

Bonus - 接口智能提示

最後介紹一個實用的小技巧。以下是一個接口的類型定義:

interface Seal {
 name: string;  url: string; } interface API {  "/user": { name: string; age: number; phone: string };  "/seals": { seal: Seal[] }; } const api = <URL extends keyof API>(url: URL): Promise<API[URL]> => {  return fetch(url).then((res) => res.json()); }; 複製代碼

咱們經過泛型以及泛型約束,實現了智能提示的功能。使用效果:

(接口名智能提示)

(接口返回智能提示)

原理很簡單,當你僅輸入 api 的時候,其會將 API interface 下的全部 key 提示給你,當你輸入某一個 key 的時候,其會根據 key 命中 interface 定義的類型,而後給予類型提示。

總結

學習 Typescript 並非一件簡單的事情,尤爲是沒有其餘語言背景的狀況。而 TS 中最爲困難的內容之一恐怕就是泛型了。

泛型和咱們平時使用的函數是很像的,若是將二者進行橫向對比,會很容易理解,不少函數的都關係能夠遷移到泛型,好比函數嵌套,遞歸,默認參數等等。泛型是對類型進行編程,參數是類型,返回值是一個新的類型。咱們甚至能夠對泛型的參數進行約束,就相似於函數的類型約束。

最後經過幾個高級的泛型用法以及若干使用的泛型工具類幫助你們理解和消化上面的知識。要知道真正的 TS 高手都是玩類型的,高手纔不會知足於類型的交叉並操做。 泛型用的好確實能夠極大減小代碼量,提升代碼維護性。若是用的太深刻,也可能會團隊成員面面相覷,一臉茫然。所以抽象層次必定要合理,不只僅是泛型,整個軟件工程都是如此。

你們也能夠關注個人公衆號《腦洞前端》獲取更多更新鮮的前端硬核文章,帶你認識你不知道的前端。

Reference

[1]

A use case for TypeScript Generics: https://juliangaramendy.dev/when-ts-generics/

[2]

React.FC Type Definition: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts

[3]

Typescript 複雜泛型實踐:如何切掉函數參數表的最後一個參數?: https://zhuanlan.zhihu.com/p/147248333

[4]

HTMLElement Type Definition: https://github.com/microsoft/TypeScript/blob/master/lib/lib.dom.d.ts

[5]

TS 官方的16 種工具類型: https://www.typescriptlang.org/docs/handbook/utility-types.html#partialt

[6]

type-fest: https://github.com/sindresorhus/type-fest

[7]

TS - es5.d.ts: https://github.com/microsoft/TypeScript/blob/master/src/lib/es5.d.ts#L1431

相關文章
相關標籤/搜索