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 团队的指南。