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