1 引言
隨著 Typescript 4 Beta 的釋出,又帶來了許多新功能,其中 Variadic Tuple Types 解決了大量過載模版程式碼的頑疾,使得這次更新非常有意義。
2 簡介
可變元組型別
考慮 concat
場景,接收兩個陣列或者元組型別,組成一個新陣列:
function concat(arr1, arr2) {
return [...arr1, ...arr2];
}
如果要定義 concat
的型別,以往我們會通過列舉的方式,先列舉第一個引數陣列中的每一項:
function concat<>(arr1: [], arr2: []): [A];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)
再列舉第二個引數中每一項,如果要完成所有列舉,僅考慮陣列長度為 6 的情況,就要定義 36 次過載,程式碼幾乎不可維護:
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(
arr1: [A1, B1, C1],
arr2: [A2]
): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(
arr1: [A1, B1, C1, D1],
arr2: [A2]
): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(
arr1: [A1, B1, C1, D1, E1],
arr2: [A2]
): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(
arr1: [A1, B1, C1, D1, E1, F1],
arr2: [A2]
): [A1, B1, C1, D1, E1, F1, A2];
如果我們採用批量定義的方式,問題也不會得到解決,因為引數型別的順序得不到保證:
function concat<T, U>(arr1: T[], arr2, U[]): Array<T | U>;
在 Typescript 4,可以在定義中對陣列進行解構,通過幾行程式碼優雅的解決可能要過載幾百次的場景:
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}
上面例子中,Arr
型別告訴 TS T
與 U
是陣列型別,再通過 [...T, ...U]
按照邏輯順序依次拼接型別。
再比如 tail
,返回除第一項外剩下元素:
function tail(arg) {
const [_, ...result] = arg;
return result;
}
同樣告訴 TS T
是陣列型別,且 arr: readonly [any, ...T]
申明瞭 T
型別表示除第一項其餘項的型別,TS 可自動將 T
型別關聯到物件 rest
:
function tail<T extends any[]>(arr: readonly [any, ...T]) {
const [_ignored, ...rest] = arr;
return rest;
}
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
// type [2, 3, 4]
const r1 = tail(myTuple);
// type [2, 3, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);
另外之前版本的 TS 只能將型別解構放在最後一個位置:
type Strings = [string, string];
type Numbers = [number, number];
// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];
如果你嘗試將 [...Strings, ...Numbers]
這種寫法,將會得到一個錯誤提示:
A rest element must be last in a tuple type.
但在 Typescript 4 版本支援了這種語法:
type Strings = [string, string];
type Numbers = number[];
// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];
對於再複雜一些的場景,例如高階函式 partialCall
,支援一定程度的柯里化:
function partialCall(f, ...headArgs) {
return (...tailArgs) => f(...headArgs, ...tailArgs);
}
我們可以通過上面的特性對其進行型別定義,將函式 f
第一個引數型別定義為有順序的 [...T, ...U]
:
type Arr = readonly unknown[];
function partialCall<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...headArgs: T
) {
return (...b: U) => f(...headArgs, ...b);
}
測試效果如下:
const foo = (x: string, y: number, z: boolean) => {};
// This doesn't work because we're feeding in the wrong type for 'x'.
const f1 = partialCall(foo, 100);
// ~~~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.
// This doesn't work because we're passing in too many arguments.
const f2 = partialCall(foo, "hello", 100, true, "oops");
// ~~~~~~
// error! Expected 4 arguments, but got 5.
// This works! It has the type '(y: number, z: boolean) => void'
const f3 = partialCall(foo, "hello");
// What can we do with f3 now?
f3(123, true); // works!
f3();
// error! Expected 2 arguments, but got 0.
f3(123, "hello");
// ~~~~~~~
// error! Argument of type '"hello"' is not assignable to parameter of type 'boolean'
值得注意的是,const f3 = partialCall(foo, "hello");
這段程式碼由於還沒有執行到 foo
,因此只匹配了第一個 x:string
型別,雖然後面 y: number, z: boolean
也是必選,但因為 foo
函式還未執行,此時只是引數收集階段,因此不會報錯,等到 f3(123, true)
執行時就會校驗必選引數了,因此 f3()
時才會提示引數數量不正確。
元組標記
下面兩個函式定義在功能上是一樣的:
function foo(...args: [string, number]): void {
// ...
}
function foo(arg0: string, arg1: number): void {
// ...
}
但還是有微妙的區別,下面的函式對每個引數都有名稱標記,但上面通過解構定義的型別則沒有,針對這種情況,Typescript 4 支援了元組標記:
type Range = [start: number, end: number];
同時也支援與解構一起使用:
type Foo = [first: number, second?: string, ...rest: any[]];
Class 從建構函式推斷成員變數型別
建構函式在類例項化時負責一些初始化工作,比如為成員變數賦值,在 Typescript 4,在建構函式裡對成員變數的賦值可以直接為成員變數推導型別:
class Square {
// Previously: implicit any!
// Now: inferred to `number`!
area;
sideLength;
constructor(sideLength: number) {
this.sideLength = sideLength;
this.area = sideLength ** 2;
}
}
如果對成員變數賦值包含在條件語句中,還能識別出存在 undefined
的風險:
class Square {
sideLength;
constructor(sideLength: number) {
if (Math.random()) {
this.sideLength = sideLength;
}
}
get area() {
return this.sideLength ** 2;
// ~~~~~~~~~~~~~~~
// error! Object is possibly 'undefined'.
}
}
如果在其他函式中初始化,則 TS 不能自動識別,需要用 !:
顯式申明型別:
class Square {
// definite assignment assertion
// v
sideLength!: number;
// ^^^^^^^^
// type annotation
constructor(sideLength: number) {
this.initialize(sideLength);
}
initialize(sideLength: number) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength ** 2;
}
}
短路賦值語法
針對以下三種短路語法提供了快捷賦值語法:
a &&= b; // a = a && b
a ||= b; // a = a || b
a ??= b; // a = a ?? b
catch error unknown 型別
Typescript 4.0 之後,我們可以將 catch error 定義為 unknown
型別,以保證後面的程式碼以健壯的型別判斷方式書寫:
try {
// ...
} catch (e) {
// error!
// Property 'toUpperCase' does not exist on type 'unknown'.
console.log(e.toUpperCase());
if (typeof e === "string") {
// works!
// We've narrowed 'e' down to the type 'string'.
console.log(e.toUpperCase());
}
}
PS:在之前的版本,catch (e: unknown)
會報錯,提示無法為 error
定義 unknown
型別。
自定義 JSX 工廠
TS 4 支援了 jsxFragmentFactory
引數定義 Fragment 工廠函式:
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}
還可以通過註釋方式覆蓋單檔案的配置:
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = (
<>
<div>Hello</div>
</>
);
以上程式碼編譯後解析結果如下:
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null, h("div", null, "Hello"));
其他升級
其他的升級快速介紹:
構建速度提升,提升了 --incremental
+ --noEmitOnError
場景的構建速度。
支援 --incremental
+ --noEmit
引數同時生效。
支援 @deprecated
註釋, 使用此註釋時,程式碼中會使用 刪除線 警告呼叫者。
區域性 TS Server 快速啟動功能, 開啟大型專案時,TS Server 要準備很久,Typescript 4 在 VSCode 編譯器下做了優化,可以提前對當前開啟的單檔案進行部分語法響應。
優化自動匯入, 現在 package.json
dependencies
欄位定義的依賴將優先作為自動匯入的依據,而不再是遍歷 node_modules
匯入一些非預期的包。
除此之外,還有幾個 Break Change:
lib.d.ts
型別升級,主要是移除了 document.origin
定義。
覆蓋父 Class 屬性的 getter 或 setter 現在都會提示錯誤。
通過 delete
刪除的屬性必須是可選的,如果試圖用 delete
刪除一個必選的 key,則會提示錯誤。
3 精讀
Typescript 4 最大亮點就是可變元組型別了,但可變元組型別也不能解決所有問題。
拿筆者的場景來說,函式 useDesigner
作為自定義 React Hook 與 useSelector
結合支援 connect redux 資料流的值,其呼叫方式是這樣的:
const nameSelector = (state: any) => ({
name: state.name as string,
});
const ageSelector = (state: any) => ({
age: state.age as number,
});
const App = () => {
const { name, age } = useDesigner(nameSelector, ageSelector);
};
name
與 age
是 Selector 註冊的,內部實現方式必然是 useSelector
+ reduce,但型別定義就麻煩了,通過過載可以這麼做:
import * as React from 'react';
import { useSelector } from 'react-redux';
type Function = (...args: any) => any;
export function useDesigner();
export function useDesigner<T1 extends Function>(
t1: T1
): ReturnType<T1> ;
export function useDesigner<T1 extends Function, T2 extends Function>(
t1: T1,
t2: T2
): ReturnType<T1> & ReturnType<T2> ;
export function useDesigner<
T1 extends Function,
T2 extends Function,
T3 extends Function
>(
t1: T1,
t2: T2,
t3: T3,
t4: T4,
): ReturnType<T1> &
ReturnType<T2> &
ReturnType<T3> &
ReturnType<T4> &
;
export function useDesigner<
T1 extends Function,
T2 extends Function,
T3 extends Function,
T4 extends Function
>(
t1: T1,
t2: T2,
t3: T3,
t4: T4
): ReturnType<T1> &
ReturnType<T2> &
ReturnType<T3> &
ReturnType<T4> &
;
export function useDesigner(...selectors: any[]) {
return useSelector((state) =>
selectors.reduce((selected, selector) => {
return {
...selected,
...selector(state),
};
}, {})
) as any;
}
可以看到,筆者需要將 useDesigner
傳入的引數通過函式過載方式一一傳入,上面的例子只支援到了三個引數,如果傳入了第四個引數則函式定義會失效,因此業界做法一般是定義十幾個過載,這樣會導致函式定義非常冗長。
但參考 TS4 的例子,我們可以避免型別過載,而通過列舉的方式支援:
type Func = (state?: any) => any;
type Arr = readonly Func[];
const useDesigner = <T extends Arr>(
...selectors: T
): ReturnType<T[0]> &
ReturnType<T[1]> &
ReturnType<T[2]> &
ReturnType<T[3]> => {
return useSelector((state) =>
selectors.reduce((selected, selector) => {
return {
...selected,
...selector(state),
};
}, {})
) as any;
};
可以看到,最大的變化是不需要寫四遍過載了,但由於場景和 concat
不同,這個例子返回值不是簡單的 [...T, ...U]
,而是 reduce
的結果,所以目前還只能通過列舉的方式支援。
當然可能存在不用列舉就可以支援無限長度的入參型別解析的方案,因筆者水平有限,暫未想到更好的解法,如果你有更好的解法,歡迎告知筆者。
4 總結
Typescript 4 帶來了更強型別語法,更智慧的型別推導,更快的構建速度以及更合理的開發者工具優化,唯一的幾個 Break Change 不會對專案帶來實質影響,期待正式版的釋出。
討論地址是:精讀《Typescript 4》· Issue #259 · dt-fe/weekly
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)
本文使用 mdnice 排版