<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【TypeScript】never 和 unknown 的優(yōu)雅之道

          共 9012字,需瀏覽 19分鐘

           ·

          2022-05-30 04:17

          關(guān)注并將「趣談前端」設(shè)為星標(biāo)

          定期推送技術(shù)干貨/優(yōu)秀開源/技術(shù)思維

          1、前言

          ?TypeScript 在版本 2.0 和 3.0 分別引入了 “never” 和 “unknown” 兩個基本類型,在引入這兩個類型之后,TypeScript 的類型系統(tǒng)得到了極大的完善。

          但在我平時接手代碼的時候,我發(fā)現(xiàn)很多同學(xué)的觀念還停留在 1.0 的時代,那個 any 大法好的時代。畢竟 JavaScript 是一門弱類型動態(tài)語言,我們以往不會投入過多的時間去關(guān)注類型設(shè)計。在引入 TypeScript 之后,我們甚至還會抱怨:“這代碼怎么還越寫越多了?”。

          其實我們應(yīng)該反過來思考,OOP 的編程范式,才是 ES6 后的代碼應(yīng)該有的模樣。

          2、TypeScript 中的 top type、bottom type

          在類型系統(tǒng)設(shè)計中,有兩種特別的類型:

          • Top type:被稱為通用父類型,也就是能夠包含所有值的類型。

          • Bottom type:代表沒有值的類型,它也被稱為類型,是所有類型的子類型。

          按照類型系統(tǒng)的解釋,在 TypeScript 3.0 中,有兩個 top type(any 和 unknown) 和一個 bottom type(never)。

          但也有一些人認(rèn)為,any 也是一個 bottom type,因為 any 也可以作為很多類型的子類型。但這種說法其實并不嚴(yán)格,我們可以深入了解一下 unknown、any、never 這三個類型。

          3、unknown 和 any

          3.1 unknown —— 代表萬物

          我在閱讀同事的代碼時,很少看到 unknown 類型的出現(xiàn)。這并不意味著它不重要,相反,它是安全版本的 any 類型。

          它和 any 的區(qū)別很簡單,參考下面的例子

          function format1(value: any) {
          value.toFixed(2); // 不飄紅,想干什么干什么,very dangerous
          }

          function format2(value: unknown) {
          value.toFixed(2); // 代碼會飄紅,阻止你這么做

          // 你需要收窄類型范圍,例如:

          // 1、類型斷言 —— 不飄紅,但執(zhí)行時可能錯誤
          (value as Number).toFixed(2);

          // 2、類型守衛(wèi) —— 不飄紅,且確保正常執(zhí)行
          if (typeof value === 'number') {
          // 推斷出類型: number
          value.toFixed(2);
          }

          // 3、類型斷言函數(shù),拋出錯誤 —— 不飄紅,且確保正常執(zhí)行
          assertIsNumber(value);
          value.toFixed(2);
          }


          /** 類型斷言函數(shù),拋出錯誤 */
          function assertIsNumber(arg: unknown): asserts arg is Number {
          if (!(arg instanceof Number)) {
          thrownewTypeError('Not a Number: ' + arg);
          }
          }

          使用 any 好比鬼屋探險,代碼執(zhí)行的時候處處見鬼。而 unknown 結(jié)合類型守衛(wèi)等方式,可以確保上游數(shù)據(jù)結(jié)構(gòu)不確定時,也能讓代碼正常執(zhí)行。

          3.2 any —— 一絲不掛

          我們用到 any,就意味著放棄類型檢查了,因為它不是用來描述具體類型的。

          在使用它之前,我們需要想兩件事:

          1. 能否使用更具體的類型

          2. 能否使用 unknown 代替

          都不能的情況下,any 才是最后的選擇。

          3.3 回顧以前的類型設(shè)計

          現(xiàn)有的一些類型設(shè)計用到了 any,其實不夠準(zhǔn)確。這里舉兩個例子:

          3.3.1 String()

          String()?能夠接受任何參數(shù),轉(zhuǎn)化為字符串。

          結(jié)合上文介紹的 unknown 類型,其實這里的參數(shù)也可以設(shè)計成 unknown,但內(nèi)部實現(xiàn)就需要多設(shè)計些類型守衛(wèi)了。但 unknown 類型是后面才出現(xiàn)的,所以一開始的設(shè)計還是采用了 any,也就是我們現(xiàn)在看到的:

          /**
          * typescript/lib/lib.es5.d.ts
          */

          interface StringConstructor {
          new(value?: any): String;
          (value?: any): string;
          readonly prototype: String;
          fromCharCode(...codes: number[]): string;
          }

          3.3.2 JSON.parse()

          最近我寫了一段涉及深拷貝的代碼:

          exportfunction deleteCommentFromComments<T>(comments: GenericsComment[], comment: GenericsComment) {
          // 深拷貝
          const list: GenericsComment[] = JSON.parse(JSON.stringify(comments));

          // 找到對應(yīng)的評論下標(biāo)
          const targetIndex = list.findIndex((item) => {
          if (item.comment_id === comment.comment_id) {
          returntrue;
          }
          returnfalse;
          });

          if (targetIndex !== -1) {
          // 剔除對應(yīng)的評論
          list.splice(targetIndex, 1);
          }

          return list;
          }

          很明顯,JSON.parse () 的輸出是隨著輸入動態(tài)改變的(甚至有可能拋出 Error),它的函數(shù)簽名被設(shè)計成了:

          interface JSON {
          parse(text: string, reviver?: (this: any, key: string, value: any) =>any): any;
          ...
          }

          這里可以用 unknown 嘛?可以,不過原因和上面一樣,JSON.parse()?的函數(shù)簽名被添加到 TypeScript 系統(tǒng)之前,unknown 類型還沒出現(xiàn),否則它的返回類型應(yīng)該是 unknown。

          4、never

          上文提到,never?類型表示的是空類型,也就是值永不存在的類型。

          值會永不存在的兩種情況:

          1. 如果一個函數(shù)執(zhí)行時拋出了異常,那么這個函數(shù)永遠(yuǎn)不存在返回值(因為拋出異常會直接中斷程序運行,這使得程序運行不到返回值那一步,即具有不可達的終點,也就永不存在返回了);

          2. 函數(shù)中執(zhí)行無限循環(huán)的代碼(死循環(huán)),使得程序永遠(yuǎn)無法運行到函數(shù)返回值那一步,永不存在返回。

          // 異常
          function err(msg: string): never { // OK
          throw new Error(msg);
          }

          // 死循環(huán)
          function loopForever(): never { // OK
          while (true) {};
          }

          4.1 唯一的 bottom type

          由于 never 是 typescript 的唯一一個 bottom type,它能夠表示任何類型的子類型,所以能夠賦值給任何類型:

          let err: never;
          let num: number = 4;

          num = err; // OK

          我們可以使用集合來理解 never,unknown 是全集,never 是最小單元(空集),任意類型都包含了 never。

          4.1.1 null/undefined 和 never

          這里可能就要問了,null 和 undefined 好像也可以表示任何類型的子類型,為啥不是 bottom type。非也,never 特殊就特殊在,除了自身以外,沒有任何類型是它的子類型,或者說可以賦值給它。它才是人下人(狗頭),我們可以用下面的例子對比看看:

          // null 和 undefined,可以被 never 賦值
          declare const n: never;

          let a: null = n; // 正確
          let b: undefined = n; // 正確

          // never 是 bottom type,除了自己以外沒有任何類型可以賦值給它
          let ne: never;

          ne = null; // 錯誤
          ne = undefined; // 錯誤

          declare const an: any;
          ne = an; // 錯誤,any 也不可以

          declareconst nev: never;
          ne = nev; // 正確,只有 never 可以賦值給 never

          上面的例子基本上說明了 null/undefined 跟 never 的區(qū)別,never 才是最 bottom 的。

          4.1.2 為什么說 any 不是嚴(yán)格的 bottom type

          我在閱讀一些文章的時候發(fā)現(xiàn),大家常說 any 既是 top type,也是 bottom type,但這種說法并不嚴(yán)謹(jǐn)。

          從上文我們知道,除了 never 自身,沒有任何類型能賦值給 never。any 是否滿足這個特性呢?顯然不能,舉個很簡單的例子:

          const a = 'anything';

          const b: any = a; // 能夠賦值
          const c: never = a; // 報錯,不能賦值

          而我們?yōu)槭裁凑f never 才是 bottom type?維基百科上這樣解釋:

          A function whose return type is bottom (presumably) cannot return any value, not even the zero size unit type. Therefore a function whose return type is the bottom type cannot return.

          返回類型為底部類型的函數(shù)不能返回任何值,甚至不能返回零大小的單元類型。因此返回類型為底部類型的函數(shù)不能返回。

          從這里我們也很容易發(fā)現(xiàn),在一個類型系統(tǒng)中,bottom type 是獨一無二的,它唯一地描述了函數(shù)無返回的情況。所以,有了 never 之后,any 這種脫離了類型檢查的異端肯定稱不上是 bottom type。

          4.2 never 的妙用

          never 有以下的使用場景:

          • Unreachable code 檢查:標(biāo)記不可達代碼,獲得編譯提示。

          • 類型運算:作為類型運算中的最小因子。

          • Exhaustive Check:為復(fù)合類型創(chuàng)造編譯提示。

          • ......

          關(guān)于 never 的用途,知乎上有個很好的討論。不可否認(rèn)的是,never 這個東西很奇妙,從集合論的角度,它是一個空集合,因此它可以通過空集合的一些特性,為我們的類型運算工作帶來很大便利。接下來來具體講講各個使用場景:

          4.2.1 Unreachable code 檢查

          一個萌新寫出了下面這行代碼:

          process.exit(0);
          console.log("hello world") // Unreachable code detected.ts(7027)

          不要笑,是真的有可能。當(dāng)然這時候如果你使用了 ts,它會給你一個編譯器提示:

          Error: Unreachable code detected.ts(7027)

          因為?process.exit()?返回類型被定義為了 never,在它之后的自然就是「unreachable code」了。

          其他可能的場景還有,監(jiān)聽套接字:

          function listen(): never {
          while(true){
          let conn = server.accept();
          }
          }

          listen();
          console.log("!!!"); // Unreachable code detected.ts(7027)

          通常來說,我們手動標(biāo)記函數(shù)返回值為 never 類型,來幫助編譯器識別「unreachable code」,并幫助我們收窄(narrow)類型。下面是一個沒標(biāo)記的例子:

          function throwError() {
          throw new Error();
          }

          function firstChar(msg: string | undefined) {
          if (msg === undefined)
          throwError();
          let chr = msg.charAt(1) // Object is possibly 'undefined'.
          }

          由于編譯器不知道 throwError 是一個無返回的函數(shù),所以?throwError()?之后的代碼被認(rèn)為在任意情況下都是可達的,讓編譯器誤會 msg 的類型是 string | undefined。

          這時候如果標(biāo)記上了 never 類型,那么 msg 的類型將會在空檢查之后收窄為 string:

          function throwError(): never {
          throw new Error();
          }

          function firstChar(msg: string | undefined) {
          if (msg === undefined)
          throwError();
          let chr = msg.charAt(1) // ?
          }

          4.2.2 類型運算

          4.2.2.1 最小因子

          上文提到 never 可以理解為一個空集,那么它將滿足下面的運算規(guī)則:

          T | never => T
          T & never =>never

          也就是說,never 是類型運算的最小因子。這些規(guī)則幫助我們簡化了一些瑣碎的類型運算,舉個例子,像?Promise.race?合并的多個?Promise,有時是無法確切知道時序和返回結(jié)果的。現(xiàn)在我們使用一個?Promise.race?來將一個有網(wǎng)絡(luò)請求返回值的?Promise?和另一個在給定時間之內(nèi)就會被?reject?的?Promise?合并起來。

          asyncfunction fetchNameWithTimeout(userId: string): Promise<string> {
          const data = await Promise.race([
          fetchData(userId),
          timeout(3000)
          ])
          return data.userName;
          }

          下面是一個 timeout 函數(shù)的實現(xiàn),如果超過指定時間,將會拋出一個 Error。由于它是無返回的,所以返回結(jié)果定義為了?Promise

          function timeout(ms: number): Promise<never> {
          return new Promise((_, reject) => {
          setTimeout(() => reject(newError("Timeout!")), ms)
          })
          }

          很好,接下來編譯器會去推斷?Promise.race?的返回值,因為 race 會取最先完成的那個?Promise?的結(jié)果,所以在上面這個例子里,它的函數(shù)簽名類似這樣:

          function race<A, B>(inputs: [Promise, Promise]): Promise<A | B>

          代入 fetchData 和 timeout 進來,A 則是?{ userName: string },而 B 則是?never。因此,函數(shù)輸出的?promise?返回值類型為?{ userName: string } | never。?又因為?never?是最小因子,可以消去。故返回值可簡化為?{ userName: string },這正是我們希望的。

          那如果在這里使用了?any?或者?unknown,結(jié)果又會怎樣呢?

          // 使用 any
          function timeout(ms: number): Promise<any> {
          ......
          }
          // { userName: string } | any => any,失去了類型檢查
          asyncfunction fetchNameWithTimeout(userId: string): Promise<string> {
          ......
          return data.userName; // ? data 被推斷為 any
          }

          any 很好理解,雖然能正常通過,但相當(dāng)于沒有類型檢查了。

          // 使用 unknown
          function timeout(ms: number): Promise<unknown> {
          ......
          }
          // { userName: string } | unknown => unknown,類型被模糊
          asyncfunction fetchNameWithTimeout(userId: string): Promise<string> {
          ......
          return data.userName; // ? data 被推斷為 unknown
          }

          unknown 則是模糊了類型,需要我們手動去收窄類型。

          當(dāng)我們嚴(yán)格使用 never 來描述「unreachable code」時,編譯器便能夠幫助我們準(zhǔn)確地收窄類型,做到代碼即文檔。

          4.2.2.2 條件類型中使用

          我們經(jīng)常在條件類型中見到 never,它被用于表示 else 的情況。

          type Arguments = T extends (...args: infer A) => any ? A : never
          type Return = T extends (...args: any[]) => infer R ? R : never

          對于上述推導(dǎo)函數(shù)參數(shù)和返回值的兩個條件類型,即使傳入的 T 是非函數(shù)類型,我們也能夠得到編譯器的提示:

          // Error: Type '3' is not assignable to type 'never'
          const x: Return<"not a function type"> = 3;

          在收窄聯(lián)合類型時,never 也巧妙地發(fā)揮了它作為最小因子的作用。比如說下面這個從?T?中排除?null?和?undefined?的例子:

          type NullOrUndefined = null | undefined
          type NonNullable = T extends NullOrUndefined ? never : T

          // 運算過程
          type NonNullable<string | null>
          // 聯(lián)合類型被分解成多個分支單獨運算
          => (string extends NullOrUndefined ? never : string) | (nullextends NullOrUndefined ? never : null)
          // 多個分支得到結(jié)果,再次聯(lián)合
          => string | never
          // never 在聯(lián)合類型運算中被消解
          => string

          4.2.3 Exhaustive Check

          聯(lián)合類型、代數(shù)數(shù)據(jù)類型等復(fù)合類型,可以結(jié)合 switch 語句來進行類型收窄:

          interface Foo {
          type: 'foo'
          }

          interface Bar {
          type: 'bar'
          }

          type All = Foo | Bar;

          function handleValue(val: All) {
          switch (val.type) {
          case'foo':
          // val 此時是 Foo
          break;
          case'bar':
          // val 此時是 Bar
          break;
          default:
          // val 此時是 never
          const exhaustiveCheck: never = val;
          break;
          }
          }

          如果后面有人修改了?All?類型,它會發(fā)現(xiàn)產(chǎn)生了一個編譯錯誤:

          type All = Foo | Bar | Baz;

          function handleValue(val: All) {
          switch (val.type) {
          case'foo':
          // val 此時是 Foo
          break;
          case'bar':
          // val 此時是 Bar
          break;
          default:
          // val 此時是 Baz
          // ? Type 'Baz' is not assignable to type 'never'.(2322)
          const exhaustiveCheck: never = val;
          break;
          }
          }

          在 default branch 里面 val 會被收窄為?Baz,導(dǎo)致無法賦值給 never,產(chǎn)生一個編譯錯誤。開發(fā)者能夠意識到 handleValue 里面需要加上針對 Baz 的處理邏輯。通過這個辦法,可以確保 handleValue 總是窮盡 (exhaust) 了 All 所有可能的類型。

          5、結(jié)語

          對重視類型規(guī)范和代碼設(shè)計的同學(xué)來說,TypeScript 絕不是枷鎖,而是一門實用主義語言。通過深入了解 never 和 unknown 在 TypeScript 類型系統(tǒng)中的使用和地位,可以學(xué)習(xí)到不少類型系統(tǒng)設(shè)計和集合論的知識,在實際開發(fā)中合理 narrow 類型,組織起可靠安全的代碼。


          ?? 看完三件事

          如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:





          從零搭建全棧可視化大屏制作平臺V6.Dooring

          從零設(shè)計可視化大屏搭建引擎

          Dooring可視化搭建平臺數(shù)據(jù)源設(shè)計剖析

          可視化搭建的一些思考和實踐

          基于Koa + React + TS從零開發(fā)全棧文檔編輯器(進階實戰(zhàn)




          點個在看你最好看




          緊追技術(shù)前沿,深挖專業(yè)領(lǐng)域
          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中日韩操逼视频 | 中文字幕北条麻妃在线 | 懂色av蜜臀av粉嫩av分享 | 黄页网站在线免费观看 | 久久人体视频 |