Nathaniel 原做,翻譯轉載自 New Frontend。html
我在大概一年前寫了一篇如何把 Node.js 項目從 JavaScript 遷移到 TypeScript 的指南。指南的閱讀量超過了七千,不過其實當時我對 JavaScript 和 TypeScript 的瞭解並不深刻,把重心更多地放到特定工具上,而沒怎麼從全局着手。最大的問題是我沒有提供遷移大型項目的解決方案。顯然,大型項目不可能在短期內重寫一切。所以,我很想分享下我最近學到的遷移項目到 TypeScript 的主要經驗。前端
遷移一個包含成千上百個文件的大型項目可能比你想象得要容易。整個過程主要分 3 步。node
注意:本文假定你已經有必定的 TypeScript 基礎,同時使用 Visual Studio Code,不然,一些地方可能不必定直接適用。git
相關代碼:https://github.com/llldar/mig...github
花了 10 個小時使用 console.log
排查問題後,你終於修復了 Cannot read property 'x' of undefined
問題,出現這個問題的緣由是調用了可能爲 undefined
的某個方法,給了你一個「驚喜」!你暗暗發誓,必定要把整個項目遷移到 TypeScript。可是看了看 lib
、util
、components
文件夾裏上萬個 JavaScript 文件,你對本身說:「等之後吧,等我有空的時候。」固然那一天永遠也不會到來,由於總有各類酷炫的新特性等着加到應用,客戶也不會由於項目是用 TypeScript 寫的就出大價錢。typescript
若是我告訴你,你能夠增量遷移到 TypeScript 並馬上從中受益呢?shell
d.ts
d.ts
是 TypeScript 的類型聲明文件,其中聲明瞭代碼中用到的對象和函數的各類類型,不包含任何具體的實現。npm
假定你在寫一個即時通信應用,在 user.js
文件裏有一個 user
變量和一些數組:json
const user = { id: 1234, firstname: 'Bruce', lastname: 'Wayne', status: 'online', }; const users = [user]; const onlineUsers = users.filter((u) => u.status === 'online'); console.log( onlineUsers.map((ou) => `${ou.firstname} ${ou.lastname} is ${ou.status}`) );
那麼對應的 user.d.ts
會是:後端
export interface User { id: number; firstname: string; lastname: string; status: 'online' | 'offline'; }
而後 message.js
裏定義了一個函數 sendMessage
:
function sendMessage(from, to, message)
那麼 message.d.ts
中相應的類型會是:
type sendMessage = (from: string, to: string, message: string) => boolean
不過,sendMessage
也許沒那麼簡單,參數的類型可能更復雜,也多是一個異步函數。
你可使用 import
引入其餘文件中定義的複雜類型,保持類型文件簡單明瞭,避免重複。
import { User } from './models/user'; type Message = { content: string; createAt: Date; likes: number; } interface MessageResult { ok: boolean; statusCode: number; json: () => Promise<any>; text: () => Promise<string>; } type sendMessage = (from: User, to: User, message: Message) => Promise<MessageResult>
注意:我這裏同時使用了 type
和 interface
,這是爲了展現如何使用它們。你在項目中應該主要使用其中一種。
如今已經有類型了,如何搭配 js
文件使用呢?
大致上有兩種方式:
假設同一文件夾下有 user.d.ts
,能夠在 user.js
文件中加入如下注釋:
/** * @typedef {import('./user').User} User */ /** * @type {User} */ const user = { id: 1234, firstname: 'Bruce', lastname: 'Wayne', status: 'online', }; /** * @type {User[]} */ const users = []; // onlineUser 的類型會被自動推斷爲 User[] const onlineUsers = users.filter((u) => u.status === 'online'); console.log( onlineUsers.map((ou) => `${ou.firstname} ${ou.lastname} is ${ou.status}`) );
確保 d.ts
文件中有相應的 import
和 export
語句,這一方式才能正確工做。不然,最終會獲得 any
類型,顯然 any
類型不會是你想要的。
在沒法使用 import
的場景下,三斜槓指令是導入類型的經典方式。
注意,你可能須要在 eslint 配置文件中加入如下內容以避免 eslint 把三斜槓指令視爲錯誤:
{ "rules": { "spaced-comment": [ "error", "always", { "line": { "markers": ["/"] } } ] } }
假設 message.js
和 message.d.ts
在同一文件夾下,能夠在 message.js
文件中加入如下三斜槓指令:
/// <reference path="./models/user.d.ts" /> (僅當使用 user 類型時才加這一行) /// <reference path="./message.d.ts" />
而後給 sendMessage
函數加上如下注釋:
/** * @type {sendMessage} */ function sendMessage(from, to, message)
接着你會發現 sendMessage
有了正確的類型,IDE 能自動補全 from
、to
、message
和函數的返回類型。
或者你也能夠這麼寫:
/** * @param {User} from * @param {User} to * @param {Message} message * @returns {MessageResult} */ function sendMessage(from, to, message)
這是 jsDoc
書寫函數簽名的風格,確定沒有上一種寫法那麼簡短。
使用三斜槓指令時,應該在 d.ts
文件中移除 import
和 export
語句,不然沒法工做。若是你須要從其餘文件中引入類型,能夠這麼寫:
type sendMessage = ( from: import("./models/user").User, to: import("./models/user").User, message: Message ) => Promise<MessageResult>;
這一差異背後的緣由是 TypeScript 把不含 import
和 export
語句的 d.ts
文件視做環境(ambient)模塊聲明,包含 import
和 export
語句的則視爲普通模塊文件,而不是全局聲明,因此沒法用於三斜槓指令。
注意,在實際項目中,選擇以上兩種方式中的一種,不要混用。
d.ts
若是項目的 JavaScript 代碼中已經有大量 jsDoc
註釋,那麼你有福了,只需如下一行命令就能自動生成類型聲明文件:
npx typescript src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types
以上命令中,全部 js 文件在 src
文件夾下,輸出的 d.ts
文件位於 types
文件夾下。
若是項目使用 babel,那麼須要在 babelrc
里加上:
{ "exclude": ["**/*.d.ts"] }
不然 *.d.ts
文件會被編譯爲 *.d.js
文件,這毫無心義。
如今你應該就能享受到 TypeScript 的益處了(自動補全),無需額外配置 IDE,也不用修改 js 代碼的邏輯。
若是項目中 70% 以上的代碼都通過以上步驟遷移後,你能夠考慮開啓類型檢查,進一步幫助檢測代碼中的小錯誤和問題。別擔憂,你仍將繼續使用 JavaScript,也就是說不用改動構建過程,也不用換庫。
開啓類型檢查的主要步驟是在項目中加上 jsconfig.json
。例如:
{ "compilerOptions": { "module": "commonjs", "target": "es5", "checkJs": true, "lib": ["es2015", "dom"] }, "baseUrl": ".", "include": ["src/**/*"], "exclude": ["node_modules"] }
關鍵在於 checkJs
須要爲真,這就爲全部項目開啓了類型檢查。
開啓後可能會碰到一大堆報錯,能夠逐一修正。
若是你但願之後再修復一些文件的類型問題,能夠在文件頭部加上 // @ts-nocheck
,TypeScript 編譯器會忽略這些文件。
若是隻想忽略某行而不是整個文件的話,可使用 // @ts-ignore
。加上這個註釋後,類型檢查會忽略下一行。
使用這兩個標記可讓你慢慢修正類型檢查錯誤。
若是用的是流行的庫,那 DefinitelyTyped
上多半已經有類型定義了,只需運行如下命令:
yarn add @types/your_lib_name --dev
或
npm i @types/your_lib_name --save-dev
注意:若是庫屬於某組織,庫名中包含 @
和 /
,那麼在安裝相應的類型定義文件時須要移除 @
和 /
,並在組織名後加上 __
,例如 @babel/core
改成 babel__core
。
若是用了一個做者 10 年前就已經中止更新的 js
庫怎麼辦?大多數 npm 模塊仍然使用 JavaScript,沒有類型信息。添加 @ts-ignore
看起來不是一個好主意,由於你但願儘量地確保類型安全。
那你就須要經過建立 d.ts
文件增補模塊定義,建議建立一個 types
文件夾,加入本身的類型定義。而後就能夠享受類型安全檢查了。
declare module 'some-js-lib' { export const sendMessage: ( from: number, to: number, message: string ) => Promise<MessageResult>; }
完成這些步驟後,類型檢查應該能很好地工做,能夠避免代碼出現不少小錯誤。
修復 95% 以上類型檢查錯誤並確保每一個庫都有相應的類型定義後,你能夠進行最後一步:正式把整個項目的代碼遷移到 TypeScript。
注意:我上一篇指南中提到的一些細節這裏就不講了。
.ts
文件如今是時候把 d.ts
文件和 js 文件合併了。因爲幾乎全部的類型檢查錯誤都已修正,類型檢查已經覆蓋全部模塊,基本上只須要把 require
改爲 import
而後把代碼和類型定義都放到 ts
文件中。完成以前的工做後,這一步至關簡單。
如今咱們須要的是 tsconfig.json
而不是 jsconfig.json
。
tsconfig.json
的例子:
{ "compilerOptions": { "target": "es2015", "allowJs": false, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "noImplicitThis": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve", "lib": ["es2020", "dom"], "skipLibCheck": true, "typeRoots": ["node_modules/@types", "src/types"], "baseUrl": ".", }, "include": ["src"], "exclude": ["node_modules"] }
{ "compilerOptions": { "sourceMap": false, "esModuleInterop": true, "allowJs": false, "noImplicitAny": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, "preserveConstEnums": true, "strictNullChecks": true, "resolveJsonModule": true, "moduleResolution": "node", "lib": ["es2018"], "module": "commonjs", "target": "es2018", "baseUrl": ".", "paths": { "*": ["node_modules/*", "src/types/*"] }, "typeRoots": ["node_modules/@types", "src/types"], "outDir": "./built", }, "include": ["src/**/*"], "exclude": ["node_modules"] }
由於這樣修改後類型檢查會變得更嚴格,因此可能須要修復一些額外的類型錯誤。
改到 TypeScript 後須要在構建流程中生成可運行的代碼,一般在 package.json
中加上這一行就行:
{ "scripts":{ "build": "tsc" } }
不過,前端項目一般用了 babel,你須要這樣設置項目:
{ "scripts": { "build": "rimraf dist && tsc --emitDeclarationOnly && babel src --out-dir dist --extensions .ts,.tsx && copyfiles package.json LICENSE.md README.md ./dist" } }
別忘了改入口文件,好比:
{ "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", }
好了,萬事俱備。
注意,dist
須要改爲你實際使用的目錄。
恭喜,代碼如今遷移到了 TypeScript,有嚴格的類型檢查保證。如今能夠享受 TypeScript 帶來的全部好處,好比自動補全、靜態類型、esnext 語法、對大型項目友好。開發體驗大大提高,維護成本大大下降。編寫項目代碼再也不是痛苦的過程,不再會碰到 Cannot read property 'x' of undefined
報錯。
替代方案:
若是你但願一會兒遷移整個項目到 TypeScript,能夠參考 airbnb 團隊的指南。