TypeScript 5.0 beta 發(fā)布:新版 ES 裝飾器、泛型參數(shù)的常量修飾、枚舉增強(qiáng)等
TypeScript 已于 2023.01.26 發(fā)布 5.0 beta 版本,你可以在 5.0 Iteration Plan 查看所有被包含的 Issue 與 PR。如果想要搶先體驗(yàn)新特性,執(zhí)行:
來(lái)安裝 beta 版本的 TypeScript,或在 VS Code 中安裝 JavaScript and TypeScript Nightly 來(lái)更新內(nèi)置的 TypeScript 支持。
本篇是筆者的第六篇 TypeScript 更新日志,上一篇是 「TypeScript 4.9 beta 發(fā)布:鴿置的 ES 裝飾器、satisfies 操作符、類型收窄增強(qiáng)、單文件級(jí)別配置等」,你可以在此賬號(hào)的創(chuàng)作中找到(或在掘金/知乎搜索林不渡),接下來(lái)筆者也將持續(xù)更新 TypeScript 的 DevBlog 相關(guān),感謝你的閱讀。
另外,由于 beta 版本與正式版本通常不會(huì)有明顯的差異,這一系列通常只會(huì)介紹 beta 版本而非正式版本。
補(bǔ)充說(shuō)明:TypeScript 并不使用 Semantic Version 規(guī)范,這意味著你不能將 x.0 作為一個(gè) Major 版本——因?yàn)樗赡懿⒉话茐男愿?,你也不能?5.x 作為一個(gè) Minor 版本——因?yàn)樗赡芫桶茐男愿隆?/p>
ECMASCript 裝飾器
ES 新版裝飾器終于在 5.0 中成功 Landing(部分關(guān)于其后續(xù)迭代的討論,請(qǐng)參考 #50820),以下對(duì)新版裝飾器 API 的介紹來(lái)自于筆者此前發(fā)表的 ECMAScript 雙月報(bào)告:裝飾器提案進(jìn)入 Stage 3 。
一個(gè)最基本的裝飾器類型定義大致是這樣的:
value 為這個(gè)裝飾器應(yīng)用處的類或類成員的值,而 context 則包含了這一被裝飾的值的上下文信息。這兩個(gè)參數(shù)都基于裝飾器實(shí)際應(yīng)用的位置來(lái)決定,如果裝飾器的調(diào)用返回了一個(gè)值(Output),那么被裝飾位置的值會(huì)被這個(gè)返回值替換掉。
對(duì)于 context 參數(shù),我們先對(duì)其內(nèi)部的屬性做一個(gè)簡(jiǎn)單介紹:
- kind,被裝飾的值的類型,如 'class' / 'method' / 'field' 等,這一屬性可以被用來(lái)校驗(yàn)裝飾器被應(yīng)用在了正確的位置,或者在同一個(gè)裝飾器中,基于實(shí)際應(yīng)用位置執(zhí)行不同的裝飾邏輯。
- name,被裝飾的值的名稱,如類名、屬性名、方法名等。
- access,其包含了這個(gè)值的 getter 與 setter,我們會(huì)在下面詳細(xì)介紹。
- isStatic 與 isPrivate,在裝飾器應(yīng)用于類成員時(shí)提供這一成員的訪問(wèn)性修飾符信息。
- addInitializer,可以通過(guò)這個(gè)屬性添加要在類實(shí)例化時(shí)執(zhí)行的邏輯。
需要注意的是,除了語(yǔ)義與參數(shù)的變化,新版的裝飾器在調(diào)用語(yǔ)法上也進(jìn)行了一些調(diào)整:
- 類表達(dá)式現(xiàn)在也可以應(yīng)用裝飾器了,如:
- 裝飾器與 export 關(guān)鍵字一同應(yīng)用的方式調(diào)整為:
以下基于當(dāng)前的裝飾器類型,對(duì)裝飾器的基本能力進(jìn)行簡(jiǎn)要介紹,類型定義來(lái)自于 5.0.0 beta 版本中的 lib.decorator.d.ts? 聲明文件,為了可讀性進(jìn)行了略微修改。另外,context.access 屬性目前暫時(shí)被禁用,正在等待 #494 的討論得出結(jié)果。
類裝飾器
類裝飾器的類型定義如下:
value 為被裝飾的 Class,你可以通過(guò)返回一個(gè)新的 Class 來(lái)完全替換掉原來(lái)的 Class?;蛘哂捎谀隳苣玫皆鹊?Class,你也可以直接返回一個(gè)它的子類:
類方法裝飾器
類方法裝飾器的類型定義如下:
其 value 參數(shù)為被裝飾的類方法,可以通過(guò)返回一個(gè)新的方法來(lái)直接在原型層面代替掉原來(lái)的方法(對(duì)于靜態(tài)方法則在 Class 的層面替換)?;蛘吣阋部梢园@個(gè)原來(lái)的方法,執(zhí)行一些額外的邏輯:
類屬性裝飾器
類屬性裝飾器的類型定義如下:
不同于上面的幾種裝飾器,屬性裝飾器的 value 并不是被裝飾的屬性的值,而是一個(gè) undefined。如果要獲取被裝飾的屬性值,你可以讓屬性裝飾器返回一個(gè)函數(shù),這個(gè)函數(shù)會(huì)在屬性被賦值時(shí)調(diào)用,拿到初始值作為入?yún)?,并可以返回一個(gè)新的值作為實(shí)際的賦值。
屬性訪問(wèn)器裝飾器與 Auto Accessor 的介紹請(qǐng)參考上面的 TC39 會(huì)議報(bào)告,這里不再展開(kāi)。同時(shí),目前新版裝飾器中并不存在參數(shù)裝飾器。
如果你想了解更多新版裝飾器的實(shí)際使用,可以參考 with-new-decorators,其中包括了使用 Babel 和 TypeScript 5.0 中的新版裝飾器應(yīng)用。
另外,你也可以參考 Mustard,這是一個(gè)基于新版裝飾器,不依賴元數(shù)據(jù)的命令行應(yīng)用構(gòu)建庫(kù)(Command Line App Builder),它的使用大概是這樣的:
以上的應(yīng)用構(gòu)建了一個(gè)使用 awesome-mustard-app 作為命令的 CLI 應(yīng)用,它的調(diào)用方式是這樣的:
如果你想體驗(yàn)一下 Mustard CLI,可以執(zhí)行以下命令:
目前 Mustard 仍在進(jìn)一步完善中,文檔 也仍在編寫中,歡迎隨手點(diǎn)個(gè) star ~
另外一點(diǎn)關(guān)于新版裝飾器需要說(shuō)明的是,舊版裝飾器的好伙伴反射元數(shù)據(jù)(Reflect Metadata)目前并不支持與新版裝飾器一同使用,筆者的猜想是反射元數(shù)據(jù)提案可能會(huì)在 3 月或更晚的 TC39 會(huì)議上進(jìn)行討論,大概率也需要進(jìn)行數(shù)輪修改才能推進(jìn)到 Stage 3。因此,如果你想提前開(kāi)始使用新版裝飾器,短期內(nèi)是指望不了元數(shù)據(jù)能力的。
然而我們知道,元數(shù)據(jù)是基于裝飾器實(shí)現(xiàn)依賴注入的重要手段,基于舊版裝飾器的框架基本都是一個(gè)套路:類的成員注入元數(shù)據(jù),然后由工廠方法在實(shí)例化這個(gè)類的同時(shí)按照元數(shù)據(jù)來(lái)初始化成員。比如使用裝飾器的 NodeJs 框架中會(huì)這么寫:
UserService? 會(huì)作為類型的元數(shù)據(jù)被注入到 userService? 上,并在實(shí)例化時(shí)注入一個(gè) UserService 實(shí)例。同時(shí) queryUser 和 addUser 會(huì)分別被注冊(cè)為 GET /query? 和 POST /create? 的請(qǐng)求處理方法。缺少了元數(shù)據(jù)的類型信息注入,在新版裝飾器中我們暫時(shí)無(wú)法優(yōu)雅地實(shí)現(xiàn) UserService? 到 userService 的注入。
而在 Mustard 中,我們的應(yīng)用場(chǎng)景暫時(shí)不依賴類型信息來(lái)實(shí)現(xiàn)實(shí)例屬性注入,而是只需要做命令行參數(shù)到屬性名的映射,然后將值注入即可。我們使用了 context.addInitializer,將裝飾器收集到的屬性名、初始值等信息強(qiáng)行替換掉實(shí)例內(nèi)的屬性值,再由工廠方法按照這個(gè) Initializer 描述來(lái)真正進(jìn)行實(shí)例的初始化。
最后,舊版的 --experimentalDecorators 選項(xiàng)將會(huì)仍然保留,如果啟用此配置,則仍然會(huì)將裝飾器視為舊版,新版的裝飾器無(wú)需任何配置就能夠默認(rèn)啟用。
泛型參數(shù)的常量修飾
在此前,函數(shù)中的泛型參數(shù)推導(dǎo)只能推導(dǎo)到基礎(chǔ)類型一級(jí)(即比字面量類型高出一個(gè)層級(jí)的類型),如 string? 、string[] 這樣:
這其實(shí)類似于使用 let 聲明變量時(shí)的自動(dòng)類型推導(dǎo)表現(xiàn)。
TypeScript 5.0 新增了對(duì)泛型參數(shù)的常量修飾(基本等價(jià)于常量斷言),被修飾的泛型參數(shù)在進(jìn)行類型信息推導(dǎo)時(shí),將推導(dǎo)到盡可能精確的字面量類型層級(jí):
可以看到這里對(duì)于數(shù)組類型的類型推斷,其實(shí)基本等價(jià)于常量斷言后的類型:每一級(jí)的數(shù)組類型都加上了 readonly 修飾。
當(dāng)被常量修飾的泛型參數(shù)為數(shù)組類型時(shí),如果其泛型約束不包含 readonly,則推導(dǎo)出的類型將回歸到泛型約束來(lái)維持其可變狀態(tài),否則才會(huì)是預(yù)期的常量推導(dǎo):
枚舉增強(qiáng)
TypeScript 5.0 中對(duì)枚舉進(jìn)行了一次全面的能力增強(qiáng),移除了此前諸如「枚舉計(jì)算成員必須位于字面量成員之后」、「僅允許在數(shù)字枚舉中定義計(jì)算成員」、「常量枚舉中不允許包含變量或表達(dá)式」的限制:
這一變化的主要原因在于,此前 TypeScript 中存在數(shù)字枚舉和字符串枚舉兩種類型的枚舉,其中數(shù)字枚舉僅允許數(shù)字類型與計(jì)算屬性成員,不允許字符串類型,而字符串枚舉又僅允許數(shù)字或字符串枚舉成員:
而在 5.0 版本中將這兩種枚舉合并成了單一的、功能更強(qiáng)大的枚舉類型,其成員允許任何常量或表達(dá)式計(jì)算,并僅為常量成員賦予字符串。同時(shí),現(xiàn)在一個(gè)枚舉類型將被視為其所有成員類型組成的聯(lián)合類型,如偽代碼 type Enum = Enum.A | Enum.B | Enum.C。
--verbatimModuleSyntax 配置
我們知道,TS 文件到 JS 文件的編譯過(guò)程主要包括三件事:類型信息擦除、語(yǔ)法降級(jí)、聲明文件生成。關(guān)于類型信息擦除,你應(yīng)該能立刻想到包括類型定義和類型簽名,但實(shí)際上這里還包含著常常被忽略的類型導(dǎo)入。
在這個(gè)例子中,我們很容易確定 User 只被作為類型使用,需要被移除(否則如果運(yùn)行時(shí)不存在一個(gè) User Class,就會(huì)出錯(cuò)了),那么,如果 user.model.ts 中有這么一行代碼:
這個(gè)時(shí)候,如果把導(dǎo)入語(yǔ)句移除,可能就會(huì)導(dǎo)致代碼運(yùn)行時(shí)出現(xiàn)異常。
一直以來(lái),為了避免對(duì)導(dǎo)入語(yǔ)句的移除影響運(yùn)行時(shí)代碼,TypeScript 使用了多種方式來(lái)確定一條導(dǎo)入語(yǔ)句能否被移除,如檢查對(duì)導(dǎo)入的使用,以及其在導(dǎo)出文件中是如何聲明的。
為了簡(jiǎn)化類型導(dǎo)入的判斷工作,TypeScript 此前引入了 import type 語(yǔ)法來(lái)聲明僅類型導(dǎo)入,或成員級(jí)別的僅類型導(dǎo)入:
對(duì)應(yīng)的,還有一系列配置來(lái)進(jìn)一步細(xì)粒度地控制行為,如 --importsNotUsedAsValues? 用于確認(rèn)類型導(dǎo)入的使用(僅類型導(dǎo)入需要被顯式標(biāo)記,而未被使用的值導(dǎo)入仍然將會(huì)保留),--preserveValueImports 用于顯式避免部分導(dǎo)入語(yǔ)句的移除(所有值導(dǎo)入都將被完整保留,避免 TypeScript 無(wú)法檢測(cè)其使用方式的情況)等等。
而在 5.0 版本,TypeScript 引入了新的配置 --verbatimModuleSyntax 來(lái)進(jìn)一步簡(jiǎn)化這些情況,它的作用就簡(jiǎn)單多了:所有非僅類型導(dǎo)入/導(dǎo)出都會(huì)被保留,而僅類型導(dǎo)入/導(dǎo)出都會(huì)被移除。
moduleResolution 相關(guān)
這一部分包括了 --moduleResolution bundler? 與 Resolution Customization Flags 的相關(guān)介紹。
TypeScript 自 4.5 版本 來(lái)一直在持續(xù)改進(jìn) NodeJs 中的 ESM 支持,先后在 4.5 beta 與 4.7 正式中引入了 .mts、.cts? 擴(kuò)展名,支持了 package.json? 中的 exports, imports, type 等字段,以及新的 compilerOption.module? 與 compilerOptions.moduleResolution? 值:node16? 與 nodenext,這些能力很好地改進(jìn)了 TS + NodeJs + Pure ESM 的研發(fā)體驗(yàn),但實(shí)際上,前端er 們并不會(huì)僅僅使用 tsc 來(lái)進(jìn)行編譯,而其他的編譯工具其實(shí)并沒(méi)有這么多彎彎繞繞。
舉例來(lái)說(shuō),NodeJs 中的 ESM 強(qiáng)制要求你的相對(duì)導(dǎo)入路徑攜帶擴(kuò)展名,即 import ns from "./mod.mjs"?(你也可以使用 --experimental-specifier-resolution=node 配置來(lái)啟用自動(dòng)地路徑解析),這主要是為了貼合 NodeJs 在服務(wù)器環(huán)境下的性能表現(xiàn)。然而大部分的構(gòu)建工具其實(shí)不要求你這么做,它會(huì)融合 ESM 與 CJS 的模塊解析策略。
在 5.0 beta 版本,TS 引入了新的 moduleResolutio: bundler 配置,它會(huì)使用 NodeJs 的模塊解析策略,支持 ESM 語(yǔ)法,但不會(huì)強(qiáng)制你使用 ESM 的這些嚴(yán)格解析規(guī)則。同時(shí) 5.0 還引入了一系列細(xì)粒度的配置項(xiàng)來(lái)便于各個(gè)運(yùn)行時(shí)與構(gòu)建工具按照自己的需求進(jìn)行調(diào)整:
- --allowImportingTsExtensions?,此配置啟用后,在相對(duì)導(dǎo)入 TS 文件時(shí)就能夠攜帶上擴(kuò)展名(.ts, .mts, .tsx,不包括 .cts ,因?yàn)?ESM 才是一家人)。但需要注意的是需要同時(shí)啟用 --noEmit 或者 --emitDeclarationOnly,這是因?yàn)檫@些文件導(dǎo)入路徑還需要被構(gòu)建工具進(jìn)行處理后才能正常使用,運(yùn)行時(shí)本身是無(wú)法
- --resolvePackageJsonExports,--resolvePackageJsonImports,這兩個(gè)配置將分別強(qiáng)制 TS 在讀取 來(lái)自 node_modules 中的導(dǎo)入時(shí)去解析 package.json 中的 exports 與 imports 字段。在 moduleResolution 被指定為 node16 / nodenext / bundler 時(shí)默認(rèn)啟用。
- --allowArbitraryExtensions?,啟用此配置后,TS 在導(dǎo)入一個(gè)非 JS/JSX/TS/TSX 擴(kuò)展名的文件時(shí),也會(huì)自動(dòng)去查找其類型聲明。如導(dǎo)入 style.css 時(shí)將嘗試加載 style.d.css.ts 聲明文件:
你可能會(huì)想,為什么是 .d.css.ts?,而不是 .css.d.ts? ? 這是因?yàn)?nbsp;file.d.ts? 通常被視為 file.js/jsx? 的聲明文件,也就是 .css 其實(shí)被視為了不完整的 JS 文件名,這實(shí)際上是錯(cuò)誤的行為。
這一配置主要是為了避免在支持這些導(dǎo)入的運(yùn)行時(shí)或者構(gòu)建工具中產(chǎn)生類型報(bào)錯(cuò)(此前我們通常通過(guò) declare module '*.css' 來(lái)實(shí)現(xiàn)),它對(duì)業(yè)務(wù)開(kāi)發(fā)確實(shí)有著明顯的意義,社區(qū)可能又要為此涌現(xiàn)出一批新活了。
- --customConditions?,NodeJs 支持在 package.json 的 exports 中指定 import / require / node / default 等值來(lái)設(shè)定其在不同條件(環(huán)境)下的文件入口。而這一配置則是為了更靈活地指定條件,如你可以在 tsconfig.json 中這樣配置:
這樣若是你的 npm 包 exports 中指定了這一條件,則它會(huì)將其視為最高優(yōu)先級(jí):
其他
類型全量導(dǎo)出
現(xiàn)在,你可以使用 export type * from 'module'? 或者 export type * as namespace from 'module' 來(lái)導(dǎo)出類型了:
JSDoc 中的 @satisfies 與 @overload
TypeScript 4.9 版本中引入了用于進(jìn)行安全 upcast 操作的 satisfies 操作符(參考 TypeScript 4.9 beta 發(fā)布:鴿置的 ES 裝飾器、satisfies 操作符、類型收窄增強(qiáng)、單文件級(jí)別配置等),而在 5.0 版本中,為了支持在 JavaScript 文件中使用 JSDoc 進(jìn)行類型檢查的使用方式,現(xiàn)在你可以使用 @satisfies 標(biāo)簽來(lái)檢查類型,但同時(shí)在上下文中保持使用從原先值推導(dǎo)出的類型(這也是 satisfies 和 類型斷言最大的差異):
另外一個(gè)在本次被添加的 JSDoc 標(biāo)簽是 @overload,它用于顯式標(biāo)明每一個(gè)函數(shù)的重載簽名,從而配合類型檢查,如以下的例子:
printValue("hello!", 123)? 是一個(gè)錯(cuò)誤的重載調(diào)用,但卻沒(méi)有提示報(bào)錯(cuò)信息,但如果為重載簽名添加 @overload 標(biāo)簽,TS 就能夠依次檢查其是否有符合的重載調(diào)用:
廢棄功能
為了更好地迎接未來(lái)的 ECMAScript 演進(jìn),TypeScript 5.0 引入了對(duì)語(yǔ)言能力或配置項(xiàng)的廢棄計(jì)劃,以 keyofStringsOnly 配置為例,在 5.0 版本開(kāi)始,啟用此配置將會(huì)獲得一條警告:
在下一階段(如 5.5 版本),你仍然可以指定 keyofStringsOnly ,它也不會(huì)拋出任何錯(cuò)誤,但實(shí)際上它已經(jīng)不再有作用。在最后一個(gè)階段(如 6.0 版本),指定此配置將會(huì)拋出一個(gè)錯(cuò)誤。
在第一階段,你可以通過(guò)設(shè)置 ignoreDeprecations: "5.0" 來(lái)關(guān)閉所有由于使用在 5.0 版本開(kāi)始廢棄的功能而產(chǎn)生的警告。
已經(jīng)確定在 5.0 版本將開(kāi)始逐步廢棄的配置項(xiàng)包括 charset,noImplicitUseStrict,keyofStringsOnly,noFallthroughCasesInSwitch? 等,以及 target: ES3? 與 module: umd/system/amd。
tsconfig.json 多繼承
TypeScript 5.0 支持了 tsconfig.json 的 extends 配置的數(shù)組類型,用于同時(shí)繼承一組已有的規(guī)則,這一能力使得你能夠?qū)⒆约旱墓蚕砼渲眠M(jìn)一步拆解,再依據(jù)實(shí)際情況進(jìn)行組合:
以上這個(gè)配置集成了包含現(xiàn)代語(yǔ)言特性配置、包含 Node 應(yīng)用配置以及包含嚴(yán)格檢查的配置文件。
單文件級(jí)別配置
TypeScript 5.0 現(xiàn)在支持單文件級(jí)別的配置,如你只想為當(dāng)前文件啟用嚴(yán)格檢查,其他文件仍然使用全局配置,可以這么做:
目前支持的規(guī)則:
- strict
- noImplicitAny
- strictNullChecks
- strictFunctionTypes
- strictBindCallApply
- strictPropertyInitialization
- noImplicitThis
- useUnknownInCatchVariables
- alwaysStrict
- noUnusedLocals
- noUnusedParameters
- exactOptionalPropertyTypes
- noImplicitReturns
- noFallthroughCasesInSwitch
- noUncheckedIndexedAccess
- noImplicitOverride
- noPropertyAccessFromIndexSignature
其配置項(xiàng)也均為小駝峰轉(zhuǎn)中劃線,如 @ts-no-property-access-from-index-signature。