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

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