標籤:

TypeScript 3.0 元組類型的用法和一些奇技淫巧

TypeScript 3.0 元組類型的用法和一些奇技淫巧

來自專欄來玩TypeScript啊,機都給你開好了!69 人贊了文章

就在三天前,Anders合併了一個PR到Typescript主分支,也就是3.0版本。從3.0版本開始,Typescript 支持函數 Rest 參數和 Spread 表達式使用元組類型。

註:以下內容主要是翻譯自 Anders 的PR。在最後面分享一些關於這個特性的實踐。

這個PR包括以下改進:

元組類型的 Rest 形參到獨立的形參的展開:

當函數的 Rest 形參具有元組類型時,元組類型被展開為一系列獨立的形參。例如,以下兩個定義是等價的:

declare function foo(...args: [number, string, boolean]): void;declare function foo(args_0: number, args_1: string, args_2: boolean): void;

元素類型的 Spread 表達式到獨立的參數的展開:

當函數調用的最後一個參數是對元組類型的一個 Spread 表達式時,Spread 表達式的結果對應一系列具有元組的元素類型的獨立參數。也就是說,以下兩種調用是等價的:

const args: [number, string, boolean] = [42, "hello", true];foo(42, "hello", true);foo(args[0], args[1], args[2]);foo(...args);

泛型支持 Rest 形參,相應的支持元組類型的推導:

Rest 形參允許具有一個約束為數組類型的泛型類型,並且類型推導可以給這樣的泛型 Rest 形參推導元組類型。這開啟了高階的部分形參列表的捕獲和散列的特性:

declare function bind<T, U extends any[], V>(f: (x: T, ...args: U) => V, x: T): (...args: U) => V;declare function f3(x: number, y: string, z: boolean): void;const f2 = bind(f3, 42); // (y: string, z: boolean) => voidconst f1 = bind(f2, "hello"); // (z: boolean) => voidconst f0 = bind(f1, true); // () => voidf3(42, "hello", true);f2("hello", true);f1(true);f0();

在以上的 f2 的定義中,類型推導分別為參數 TUV 推出了類型 number[string, boolean]void

注意當一個元組類型是從一系列形參推導出,然後展開為一個參數列表時,正如例子中的 U,原本的形參名稱會被用於展開的(然而,命名並沒有語義上的意義,也無法以其他方式觀察到)。

元組類型支持可選元素類型:

元組類型如今允許在元素類型上後綴一個 ? 來指定元素是可選的:

let t: [number, string?, boolean?];t = [42, "hello", true];t = [42, "hello"];t = [42];

--strictNullChecks 模式下,? 會自動在元素類型中包含 undefined,類似於(函數的)可選參數。

元組類型允許忽略一個後綴了 ? 修飾符的元素,同時,在其右邊的所有元素也應該具有 ? 修飾符。

當嘗試把可選形參推導為元組類型時,可選形參在推出的類型中成為可選元組元素。

具有可選元素的元組類型的 length 屬性(的類型)是一個表示可能的長度的數字字面量類型的 union 類型。例如,在元組類型 [number, string?, boolean?] 中,length 屬性的類型是 1 | 2 | 3

元組類型支持 Rest 元素:

元組類型的最後一個元素可以是形式為 ...X 的 Rest 元素,X 是一個數組類型。Rest 元素指定了元組類型是無限擴展的,可能有零個或更多個具有數組元素類型的額外元素。例如,[number, ...string[]] 表示元組具有一個 number 元素和隨後任意多個 string 元素。

function tuple<T extends any[]>(...args: T): T { return args;}const numbers: number[] = getArrayOfNumbers();const t1 = tuple("foo", 1, true); // [string, number, boolean]const t2 = tuple("bar", ...numbers); // [string, ...number[]]

具有 Rest 元素的元組類型的 length 屬性的類型是 number

一些相關實踐:

通過 conditional type,我們可以對元組進行一些操作,比如取得第一個元素:

type Head<Tuple extends any[]> = Tuple extends [infer H, ...any[]] ? H : never;

但要是想從元組中去掉第一個元素呢?

type Tail<Tuple extends any[]> = Tuple extends [any, infer R] ? R : never;type x = Tail<[1, 2, 3, 4]>; // never

結果是never,因為元組不支持元素類型的命名,無法使用 ... 修飾符,所以這裡其實只有具有兩個元素的元組才會符合條件。

好在現在函數的 Rest 參數可以使用元組類型了,函數參數列表是具有命名的:

type Tail<Tuple extends any[]> = ((...x: Tuple) => void) extends ((h: any, ...rest: infer R) => void) ? R : never;type x = Tail<[1, 2, 3, 4]>; // [2, 3, 4]

同理,通過構造一個具有插入元素和元組作為參數的函數,我們也可以往元組開頭插入一個元素:

type Unshift<Tuple extends any[], Element> = ((h: Element, ...tuple: Tuple) => void) extends (...x: infer R) => void ? R : never;type x = Unshift<[1, 2, 3], 0>; // [0, 1, 2, 3]

Edit: Anders 在 PR 中回復了他的看法,以下部分的做法實際上利用了 Typescript 的 type checker 現在的表現行為,並且未來可能和遞歸檢查衝突,所以以下內容只供娛樂。

我們之所以可以操作元組類型的頭部,其實完全是因為 Rest 語法必須在最後邊的特性,如果想要操作尾部怎麼辦呢?目前來看並沒有直接的方法,所以只能換個思路。

例如,我們想要取得元組中最後一個元素,只要不斷的去掉第一個元素,直到剩下最後一個,把它取出來就可以了。

type Last<Tuple extends any[]> = { 1: Tuple[0], 0: Last<Tail<Tuple>>}[Tuple extends [any] ? 1 : 0];type x = Last<[1, 2, 3, 4]>; // 4

這裡其實用到了一個特性,對象的屬性或基類的類型是可以遞歸引用對象類型自身的,但是需要注意,只有尾遞歸是合法的,而且索引器語法里的類型需要是惰性推導的,一個通常的例子就是conditional type,它的類型依賴於泛型參數實際傳入的類型。即使這樣,這個特性仍然具有一個限制,泛型類型實例化最大不能超過100層。

現在讓我們發揮一下,嘗試將一個元組類型反轉:

type Reverse<Tuple extends any[]> = Reverse_<Tuple, []>;type Reverse_<Tuple extends any[], Result extends any[]> = { 1: Result, 0: Reverse_<Tail<Tuple>, Unshift<Result, Head<Tuple>>>}[Tuple extends [] ? 1 : 0];type x = Reverse<[1, 2, 3]>; // [3, 2, 1]

將元組中的元素從頭部一個一個取出並插入到一個新的元組中就得到反轉過的元組。這裡又用到了一個技巧,我們把結果通過一個泛型參數傳遞給遞歸調用的類型,為了最終的類型定義沒有多餘的泛型參數額外定義了一個中間版本。也可以選擇不這樣做,直接增加一個額外泛型參數並默認值來初始化結果。

現在,我們可以進一步嘗試原來不可能的事情了,如何往元組結尾加入一個元素。其實很簡單,將一個元組反轉後在頭部插入一個元素再反轉回來即可。但是很可惜,直接這樣做將導致無限泛型類型實例化觸發錯誤。

// 將會導致相關類型內出現實例化過深錯誤type Push<Tuple extends any[], Element> = Reverse<Unshift<Reverse<Tuple>, Element>>;

這其實是兩次調用 Reverse 導致的,要想繞過這個問題其實也很簡單,我們將一次 Reverse 的調用放在泛型參數初始化中去即可,但是會出現一個新的問題:

// R 不滿足 any[] 約束type Push<Tuple extends any[], Element, R = Reverse<Tuple>> = Reverse<Unshift<R, Element>>;// 泛型實例化過深type Push<Tuple extends any[], Element, R extends any[] = Reverse<Tuple>> = Reverse<Unshift<R, Element>>;

如果我們不給作為中間類型的泛型參數 R 約束,就無法用於 Unshift,如果加了約束,就又會出現實例化過深的錯誤。好在Typescript的類型系統其實是有很多漏洞的:

type ToTuple<T> = T extends any[] ? T : any[];type Push<Tuple extends any[], Element, R = Reverse<Tuple>, T extends any[]= ToTuple<R>> = Reverse<Unshift<T, Element>>;type x = Push<[1, 2, 3], 4>; // [1, 2, 3, 4]

我們增加了第二個泛型參數作為中間類型,並對 R 使用 conditional type 使其變成惰性推導的,防止出現泛型實例化過深錯誤。不過有一點遺憾的是,這樣定義出來的 Push 必須要直接調用,否則會因為泛型參數默認值不能正確初始化導致推出 any

我還想進一步嘗試實現連接兩個元組,理論上把 Push 改為遞歸的就可以了,但是如果不能在其他定義中調用的話意義不大,待有時間再嘗試一下。

PS: 編輯器出bug丟了大片文字,在md里寫好後上傳發現報跨域錯誤,用fiddler換掉HTTP響應頭終於上傳成功....順便宣傳一下群:723471730。

Edit:之前 Push 的定義有問題,修改了一下修復了,由於存在遞歸層數限制,測試一下最多可以操作多大的元組,結果是 98 個:

type x = Push<[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 10 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 20 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 30 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 40 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 50 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 60 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 70 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 80 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // 90 0, 1, 2, 3, 4, 5, 6, 7 // 98], 1>

如果為 UnshiftHeadTail 定義多個一次操作多個元素的不同版本,然後根據大小調用,可以把這個數量擴大到理想的程度,暫時就不詳述了。

Edit:試了下連接兩個元組也很簡單

type Concat<Tuple1 extends any[], Tuple2 extends any[], R = Reverse<Tuple1>, T extends any[]= ToTuple<R>> = Concat_<T, Tuple2>;type Concat_<Tuple1 extends any[], Tuple2 extends any[]> = { 1: Reverse<Tuple1>, 0: Concat_<Unshift<Tuple1, Head<Tuple2>>, Tail<Tuple2>>,}[Tuple2 extends [] ? 1 : 0];type x = Concat<[1, 2, 3], [4, 5, 6]>; // [1, 2, 3, 4, 5, 6]

推薦閱讀:

Vue + TypeScript踩坑(初始化項目)
襁褓中的 deno (番外):現狀與展望

TAG:TypeScript |