TypeScript4 大版本更新,到底有哪些新特性!
1 引言
隨著 Typescript 4 Beta 的發(fā)布,又帶來了許多新功能,其中 Variadic Tuple Types 解決了大量重載模版代碼的頑疾,使得這次更新非常有意義。
2 簡介
可變元組類型
考慮 concat 場景,接收兩個數(shù)組或者元組類型,組成一個新數(shù)組:
function?concat(arr1,?arr2)?{
??return?[...arr1,?...arr2];
}
如果要定義 concat 的類型,以往我們會通過枚舉的方式,先枚舉第一個參數(shù)數(shù)組中的每一項:
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];)
再枚舉第二個參數(shù)中每一項,如果要完成所有枚舉,僅考慮數(shù)組長度為 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];
如果我們采用批量定義的方式,問題也不會得到解決,因為參數(shù)類型的順序得不到保證:
function?concat<T,?U>(arr1:?T[],?arr2,?U[]):?Array<T?|?U>;
在 Typescript 4,可以在定義中對數(shù)組進行解構(gòu),通過幾行代碼優(yōu)雅的解決可能要重載幾百次的場景:
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 是數(shù)組類型,再通過 [...T, ...U] 按照邏輯順序依次拼接類型。
再比如 tail,返回除第一項外剩下元素:
function?tail(arg)?{
??const?[_,?...result]?=?arg;
??return?result;
}
同樣告訴 TS T 是數(shù)組類型,且 arr: readonly [any, ...T] 申明了 T 類型表示除第一項其余項的類型,TS 可自動將 T 類型關(guān)聯(lián)到對象 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 只能將類型解構(gòu)放在最后一個位置:
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]
type?Unbounded?=?[...Strings,?...Numbers,?boolean];
對于再復(fù)雜一些的場景,例如高階函數(shù) partialCall,支持一定程度的柯里化:
function?partialCall(f,?...headArgs)?{
??return?(...tailArgs)?=>?f(...headArgs,?...tailArgs);
}
我們可以通過上面的特性對其進行類型定義,將函數(shù) f 第一個參數(shù)類型定義為有順序的 [...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"); 這段代碼由于還沒有執(zhí)行到 foo,因此只匹配了第一個 x:string 類型,雖然后面 y: number, z: boolean 也是必選,但因為 foo 函數(shù)還未執(zhí)行,此時只是參數(shù)收集階段,因此不會報錯,等到 f3(123, true) 執(zhí)行時就會校驗必選參數(shù)了,因此 f3() 時才會提示參數(shù)數(shù)量不正確。
元組標(biāo)記
下面兩個函數(shù)定義在功能上是一樣的:
function?foo(...args:?[string,?number]):?void?{
??//?...
}
function?foo(arg0:?string,?arg1:?number):?void?{
??//?...
}
但還是有微妙的區(qū)別,下面的函數(shù)對每個參數(shù)都有名稱標(biāo)記,但上面通過解構(gòu)定義的類型則沒有,針對這種情況,Typescript 4 支持了元組標(biāo)記:
type?Range?=?[start:?number,?end:?number];
同時也支持與解構(gòu)一起使用:
type?Foo?=?[first:?number,?second?:?string,?...rest:?any[]];
Class 從構(gòu)造函數(shù)推斷成員變量類型
構(gòu)造函數(shù)在類實例化時負責(zé)一些初始化工作,比如為成員變量賦值,在 Typescript 4,在構(gòu)造函數(shù)里對成員變量的賦值可以直接為成員變量推導(dǎo)類型:
class?Square?{
??//?Previously:?implicit?any!
??//?Now:?inferred?to?`number`!
??area;
??sideLength;
??constructor(sideLength:?number)?{
????this.sideLength?=?sideLength;
????this.area?=?sideLength?**?2;
??}
}
如果對成員變量賦值包含在條件語句中,還能識別出存在 undefined 的風(fēng)險:
class?Square?{
??sideLength;
??constructor(sideLength:?number)?{
????if?(Math.random())?{
??????this.sideLength?=?sideLength;
????}
??}
??get?area()?{
????return?this.sideLength?**?2;
????//?????~~~~~~~~~~~~~~~
????//?error!?Object?is?possibly?'undefined'.
??}
}
如果在其他函數(shù)中初始化,則 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 參數(shù)定義 Fragment 工廠函數(shù):
{
??"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?=?(
??<>
????Hello</div>
??>
);
以上代碼編譯后解析結(jié)果如下:
//?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"));
其他升級
其他的升級快速介紹:
構(gòu)建速度提升,提升了 --incremental + --noEmitOnError 場景的構(gòu)建速度。
支持 --incremental + --noEmit 參數(shù)同時生效。
支持 @deprecated 注釋, 使用此注釋時,代碼中會使用 刪除線 警告調(diào)用者。
局部 TS Server 快速啟動功能, 打開大型項目時,TS Server 要準(zhǔn)備很久,Typescript 4 在 VSCode 編譯器下做了優(yōu)化,可以提前對當(dāng)前打開的單文件進行部分語法響應(yīng)。
優(yōu)化自動導(dǎo)入, 現(xiàn)在 package.json dependencies 字段定義的依賴將優(yōu)先作為自動導(dǎo)入的依據(jù),而不再是遍歷 node_modules 導(dǎo)入一些非預(yù)期的包。
除此之外,還有幾個 Break Change:
lib.d.ts 類型升級,主要是移除了 document.origin 定義。
覆蓋父 Class 屬性的 getter 或 setter 現(xiàn)在都會提示錯誤。
通過 delete 刪除的屬性必須是可選的,如果試圖用 delete 刪除一個必選的 key,則會提示錯誤。
3 精讀
Typescript 4 最大亮點就是可變元組類型了,但可變元組類型也不能解決所有問題。
拿筆者的場景來說,函數(shù) useDesigner 作為自定義 React Hook 與 useSelector 結(jié)合支持 connect redux 數(shù)據(jù)流的值,其調(diào)用方式是這樣的:
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 注冊的,內(nèi)部實現(xiàn)方式必然是 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 傳入的參數(shù)通過函數(shù)重載方式一一傳入,上面的例子只支持到了三個參數(shù),如果傳入了第四個參數(shù)則函數(shù)定義會失效,因此業(yè)界做法一般是定義十幾個重載,這樣會導(dǎo)致函數(shù)定義非常冗長。
但參考 TS4 的例子,我們可以避免類型重載,而通過枚舉的方式支持:
type?Func?=?(state?:?any)?=>?any;
type?Arr?=?readonly?Func[];
const?useDesigner?=?extends?Arr>(
??...selectors:?T
):?ReturnType0]>?&
??ReturnType1]>?&
??ReturnType2]>?&
??ReturnType3]>?=>?{
??return?useSelector((state)?=>
????selectors.reduce((selected,?selector)?=>?{
??????return?{
????????...selected,
????????...selector(state),
??????};
????},?{})
??)?as?any;
};
可以看到,最大的變化是不需要寫四遍重載了,但由于場景和 concat 不同,這個例子返回值不是簡單的 [...T, ...U],而是 reduce 的結(jié)果,所以目前還只能通過枚舉的方式支持。
當(dāng)然可能存在不用枚舉就可以支持無限長度的入?yún)㈩愋徒馕龅姆桨福蚬P者水平有限,暫未想到更好的解法,如果你有更好的解法,歡迎告知筆者。
4 總結(jié)
Typescript 4 帶來了更強類型語法,更智能的類型推導(dǎo),更快的構(gòu)建速度以及更合理的開發(fā)者工具優(yōu)化,唯一的幾個 Break Change 不會對項目帶來實質(zhì)影響,期待正式版的發(fā)布。
最后
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進技術(shù)群,長期交流學(xué)習(xí)...
關(guān)注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。

點個在看支持我吧,轉(zhuǎn)發(fā)就更好了
