迁移到 TypeScript - 高级指南
大约一年前,我写了一篇关于如何在 Node.js 上从 JavaScript 迁移到 TypeScript 的指南,浏览量超过 7000 次。当时我对 JavaScript 和 TypeScript 都了解不多,可能过于关注某些工具,而忽略了全局。最大的问题是,我没有提供迁移大型项目的解决方案,因为大型项目显然不可能在短时间内重写所有内容,因此我迫切地想分享我学到的关于如何迁移到 TypeScript 的最新、最棒的经验。
将你庞大的数千个文件的 Mono Repo 项目迁移到 TypeScript 的整个过程比你想象的要简单。以下是 3 个主要步骤。
注意:本文假设您了解 TypeScript 的基础知识并使用Visual Studio Code
,如果不了解,某些细节可能不适用。
本指南的相关代码:https://github.com/llldar/migrate-to-typescript-the-advance-guide
开始输入
经过10个小时的调试console.log
,你终于修复了那个Cannot read property 'x' of undefined
错误,结果发现它是由于调用了某个可能是 的方法undefined
:真是意外!你暗自发誓要把整个项目迁移到 TypeScript。但是,当你看到lib
、util
和components
文件夹以及里面成千上万的 JavaScript 文件时,你心想:“也许以后,也许等我有时间的时候再说吧。” 当然,那一天永远不会到来,因为你总是有“很酷的新功能”要添加到应用中,而且客户无论如何也不会为 TypeScript 支付更多费用。
现在,如果我告诉您可以逐步迁移到 TypeScript 并立即开始从中受益,您会怎么做?
添加魔法d.ts
d.ts
文件是来自 typescript 的类型声明文件,它们所做的只是声明代码中使用的各种类型的对象和函数,并不包含任何实际逻辑。
现在考虑您正在编写一个消息应用程序:
假设您有一个名为的常量user
和其中的一些数组user.js
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';
}
然后你就有了这个名为sendMessage
inside的函数message.js
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
文件一起工作?
一般有2种方法:
Jsdoc typedef 导入
假设它们位于同一文件夹中,则在您的文件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 would automatically infer its type to be User[]
const onlineUsers = users.filter((u) => u.status === 'online');
console.log(
onlineUsers.map((ou) => `${ou.firstname} ${ou.lastname} is ${ou.status}`)
);
要正确使用此方法,您需要将import
和export
保留在文件中d.ts
。否则,您最终会得到any
type,这绝对不是您想要的。
三斜线指令
当您在某些情况下import
无法使用三斜线指令时,它是 TypeScript 中的“好方法” 。import
eslint config file
注意:您可能需要在处理时添加以下内容triple slash directive
以避免出现 eslint 错误。
{
"rules": {
"spaced-comment": [
"error",
"always",
{
"line": {
"markers": ["/"]
}
}
]
}
}
对于消息功能,将以下内容添加到您的message.js
文件中,假设message.js
和message.d.ts
位于同一文件夹中
/// <reference path="./models/user.d.ts" /> (add this only if you use user type)
/// <reference path="./message.d.ts" />
然后在函数jsDoc
上方添加注释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
函数签名的惯例。但肯定更冗长。
使用时,您应该从文件中triple slash directive
删除import
和,否则将不起作用,如果您必须从另一个文件导入某些内容,请使用如下方法:export
d.ts
triple slash directive
type sendMessage = (
from: import("./models/user").User,
to: import("./models/user").User,
message: Message
) => Promise<MessageResult>;
所有这些背后的原因是,d.ts
如果文件没有任何导入或导出,TypeScript 会将其视为环境模块声明。如果文件包含import
或export
,则会将其视为普通模块文件,而不是全局模块文件,因此在triple slash directive
或中使用它们augmenting module definitions
将不起作用。
注意:在实际项目中,请坚持使用其中一个import and export
,triple slash directive
不要同时使用它们。
自动生成d.ts
如果你的 JavaScript 代码中已经有很多jsDoc
注释,那么你很幸运,只需一行简单的
npx typescript src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types
假设所有 js 文件都在src
文件夹中,则输出d.ts
文件将位于types
文件夹中
Babel 配置(可选)
如果你的项目中已经设置了 Babel,你可能需要将其添加到你的babelrc
{
"exclude": ["**/*.d.ts"]
}
避免将*.d.ts
文件编译成*.d.js
,这没有任何意义。
现在,您应该能够从 typescript(自动完成)中受益,而无需对 js 代码进行任何配置和逻辑更改。
类型检查
在上述步骤覆盖了至少 70% 以上的代码库之后,你现在可以考虑启用类型检查,这有助于进一步消除代码库中的小错误和 bug。别担心,你仍然会使用 JavaScript 一段时间,这意味着构建过程和库都不需要进行任何更改。
您需要做的主要事情是添加jsconfig.json
到您的项目中。
基本上,它是一个定义项目范围并定义您将要使用的库和工具的文件。
示例jsonconfig.json
文件:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"checkJs": true,
"lib": ["es2015", "dom"]
},
"baseUrl": ".",
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
这里的重点是我们需要checkJs
真实,这样我们就可以对所有js
文件启用类型检查。
增量类型检查
// @ts-nocheck
在一个文件中,如果您有一些js
文件希望稍后修复,您可以// @ts-nocheck
在页面的顶部,而 TypeScript 编译器就会忽略该文件。
// @ts-忽略
如果你只想忽略一行而不是整个文件怎么办?使用// @ts-ignore
。它只会忽略其下面的一行。
// @ts-expect-error
它类似于@ts-ignore
,但更好。它允许 TypeScript 编译器在某个地方不再出现错误时发出警告,这样你就知道该删除这条注释了。
这三个标签结合起来应该可以让您以稳定的方式修复代码库中的类型检查错误。
外部库
维护良好的图书馆
如果您正在使用一个流行的库,那么很可能已经在为其输入内容DefinitelyTyped
,在这种情况下,只需运行:
yarn add @types/your_lib_name --dev
或者
npm i @types/your_lib_name --save-dev
@
注意:如果您正在为名称包含和/
之类的组织库安装类型声明,@babel/core
则应将其名称更改为__
在中间添加 并删除@
和/
,结果类似于babel__core
。
纯 Js 库
如果你使用的js
库是作者 10 年前归档的,并且没有提供任何 TypeScript 类型支持,会怎么样?这种情况很可能发生,因为大多数 npm 模型仍然使用 JavaScript。添加 TypeScript 类型支持@ts-ignroe
似乎不是一个好主意,因为你希望尽可能保证类型安全。
现在你需要augmenting module definitions
创建一个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
文件中。鉴于你之前所做的所有工作,这个过程应该相当简单。
将 jsconfig 更改为 tsconfig
现在你需要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"]
}
由于类型检查变得更加严格,因此修复此更改后的任何附加类型检查错误。
更改 CI/CD 管道和构建流程
您的代码现在需要一个构建过程来生成可运行的代码,通常将其添加到您的代码中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 语法和出色的可扩展性。DX 正在飞速发展,而维护成本却保持最低。项目开发不再是一个痛苦的过程,您也Cannot read property 'x' of undefined
再也不会遇到类似的错误了。
替代方法:
如果你想以更“全能”的方式迁移到 TypeScript,这里有一个很棒的指南,由 airbnb 团队提供
文章来源:https://dev.to/natelindev/migrate-to-typescript-the-advance-guide-1df6