1. TypeScript4 大版本更新,到底有哪些新特性!

        共 8783字,需瀏覽 18分鐘

         ·

        2020-12-03 12:37

        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 TU 是數(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);
        };

        nameage 是 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è)小忙:

        1. 點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)

        2. 歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

        3. 關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。


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



        瀏覽 79
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 一区二区精品高清 | 乳女教师欲乱动漫无修版在线观看 | 涩涩的视频在线观看 | 农村+肉+屁股+粗+大+岳电影 | 69er小视频 |