迁移策略
将大型项目从JavaScript转换为TypeScript具有挑战性。在开始解决它之前,我们研究了两种从JS切换到TS的策略。
▍1。混合迁移策略
使用这种方法,可以将项目逐步逐个文件地转换为TypeScript。在此过程中,将编辑文件,更正键入错误,并以这种方式工作,直到将整个项目转换为TS。allowJS参数允许您在项目中同时包含TypeScript文件和JavaScript文件。因此,这种将JS项目转换为TS的方法非常可行。
使用混合迁移策略,您无需暂停开发过程,可以逐个文件地逐步将项目转换为TypeScript。但是,如果我们谈论的是大型项目,则此过程可能需要很长时间。它还需要对整个组织的程序员进行培训。需要向程序员介绍项目的细节。
▍2。综合迁移策略
这种方法将一个项目完全用JavaScript编写,或者将其中一部分用TypeScript编写,然后将其完全转换为TypeScript项目。在这种情况下,您将需要使用type
any
和comments @ts-ignore
,这将使项目可以正确编译。但是随着时间的流逝,可以对代码进行编辑并继续使用更适合的类型。
与混合策略相比,总体TypeScript迁移策略具有多个重要优势:
- . , , . , TypeScript, , .
- , . , , , . .
考虑到上述情况,在所有方面似乎普遍迁移优于混合迁移。但是,以无所不包的方式将成熟的代码库转换为TypeScript是一项非常困难的任务。为了解决这个问题,我们决定采用脚本来修改代码,即所谓的“ codemods”(codemods)。刚开始将项目手动转换为TypeScript时,我们注意到可以自动执行的重复操作。我们为这些操作中的每一个编写了代码mod,并将它们组合成一个迁移管道。
经验告诉我们,我们不能百分百确定将项目自动转换为TypeScript后,其中没有错误。但是我们发现,以下所述的步骤组合可以为我们带来最佳效果,并且最终获得了一个没有错误的TypeScript项目。使用代码模块,我们能够将一个包含50,000行代码并由1000多个文件表示的项目转换为TypeScript。我们花了一天的时间来做到这一点。
根据下图所示的管道,我们创建了ts-migrate工具。
Ts-migrate codemods
Airbnb的前端大部分使用React编写。这就是为什么代码mod的某些部分与特定于React的概念相关的原因。ts-migrate工具可以与其他库或框架一起使用,但这将需要其他配置和测试。
迁移过程概述
让我们逐步介绍将项目从JavaScript转换为TypeScript所需遵循的主要步骤。让我们谈谈这些步骤是如何实现的。
▍步骤1
每个TypeScript项目创建的第一件事是一个
tsconfig.json
。如果需要,Ts-migrate可以自己完成。此文件有一个标准模板。此外,还有一个验证系统以确保所有项目的配置均一致。这是基本配置的示例:
{
"extends": "../typescript/tsconfig.base.json",
"include": [".", "../typescript/types"]
}
▍步骤2
将文件
tsconfig.json
放置在应有的位置后,将重命名源文件。即,.js / .jsx扩展名更改为.ts / .tsx。这一步很容易实现自动化。这使您摆脱了很多体力劳动。
▍步骤3
现在该运行代码mod了!我们称它们为插件。ts-migrate的插件是代码mod,可以通过TypeScript语言服务器访问其他信息。插件接受字符串作为输入并返回修改后的字符串。jscodeshift工具箱,TypeScript API,字符串处理工具或其他AST修改工具可用于执行代码转换。
完成上述每个步骤后,我们检查以查看Git历史记录中是否有任何待处理的更改,并将其包含在项目中。这使您可以将迁移PR分成提交,从而更容易了解正在发生的事情并有助于跟踪文件名的更改。
组成ts-migrate的软件包概述
我们将ts-migrate分为3个软件包:
通过这样做,我们能够将代码转换逻辑与系统核心分开,并能够创建许多旨在解决不同问题的配置。现在,我们有两个主要配置:migration和reignore。
应用配置的目的是将
migration
项目从JavaScript转换为TypeScript。reignore
通过简单地忽略任何错误,可以使用该配置来编译项目。当您具有大型代码库并对其执行各种操作时,此配置很有用,例如:
- TypeScript版本更新。
- 对代码进行重大更改或重构代码库。
- 一些常用库的改进类型。
通过这种方法,即使产生了我们不打算立即处理的编译错误,我们也可以将项目转换为TypeScript。这也使更新TypeScript或代码中使用的库变得更加容易。
两种配置都在
ts-migrate-server
具有两部分的服务器上运行:
- TSServer:服务器的这一部分与VSCode用于在编辑器和语言服务器之间进行通信的部分非常相似。TypeScript语言服务器的新实例在单独的过程中启动。开发工具使用语言协议与之交互。
- 迁移工具:这是执行迁移过程并协调该过程的代码。该工具接受以下参数:
interface MigrateParams {
rootDir: string; // .
config: MigrateConfig; // ,
// .
server: TSServer; // TSServer.
}
该工具执行以下操作:
- 解析文件
tsconfig.json
。 - 使用源代码创建.ts文件。
- 将每个文件发送到TypeScript语言服务器以诊断该文件。有三种类型的诊断,这让我们的编译器:
semanticDiagnostics
,syntacticDiagnostics
和suggestionDiagnostics
。我们使用这些检查在源代码中查找问题区域。根据文件中唯一的诊断代码和行号,我们可以确定可能的问题类型并应用必要的代码修改。 - 通过所有插件处理每个文件。如果插件主动更改了文件中的文本,我们将更新原始文件的内容,并通知语言服务器文件已更改。
使用示例
ts-migrate-server
可以在examples软件包或main软件包中找到。它还ts-migrate-example
包含基本的插件示例。它们分为3个主要类别:
该存储库包含一组示例,旨在演示创建所有类型的简单插件的过程。它还显示了它们在组合c中的使用
ts-migrate-server
。这是转换代码的迁移管道的示例。在其输入处收到以下代码:
function mult(first, second) {
return first * second;
}
他给出了以下内容:
function tlum(tsrif: number, dnoces: number): number {
console.log(`args: ${arguments}`);
return tsrif * dnoces;
}
在此示例中,ts-migrate执行了3个转换:
- 它将所有标识符中的字符顺序颠倒
first -> tsrif
。 - 在函数声明中添加了有关类型的信息:
function tlum(tsrif, dnoces) -> function tlum(tsrif: number, dnoces: number): number
。 - 将行添加到代码中
console.log(‘args:${arguments}’);
通用插件
真正的插件位于单独的软件包ts-migrate-plugins中。让我们看看其中的一些。我们有两个基于jscodeshift的插件:
explicitAnyPlugin
和declareMissingClassPropertiesPlugin
。该jscodeshift工具包可以让你转换的AST使用常规代码重铸包。我们可以使用该函数toSource()
直接更新文件中包含的源代码。explicitAnyPlugin
插件从TypeScript语言服务器检索有关所有错误以及检测到这些错误的行的信息。然后将类型注释添加到这些行。这种方法允许您修复错误,因为使用了
semanticDiagnostics
any
any
使您摆脱编译错误。
这是处理之前的一些示例代码:
const fn2 = function(p3, p4) {}
const var1 = [];
这是插件处理的相同代码:
const fn2 = function(p3: any, p4: any) {}
const var1: any = [];
该declareMissingClassPropertiesPlugin采取错误代码的诊断消息
2339
(你能猜出这个代码方式吗?),如果可以找到类的声明缺少标识,将它们添加到注释类的主体any
。从插件的名称,我们可以得出结论,它仅适用于ES6类。
下一类插件基于AST TypeScript。通过处理AST,我们可以生成一系列要对源文件进行的更新。这些更新的说明如下所示:
type Insert = { kind: 'insert'; index: number; text: string };
type Replace = { kind: 'replace'; index: number; length: number; text: string };
type Delete = { kind: 'delete'; index: number; length: number };
生成有关必要更新的信息后,仅保留以相反的顺序将其输入文件中。如果执行此操作后,我们收到新的程序代码,则将相应地更新源代码文件。
让我们看一下接下来的几个基于AST的插件。这是
stripTSIgnorePlugin
和hoistClassStaticsPlugin
。stripTSIgnorePlugin
插件是迁移管道中使用的第一个插件。它将删除文件中的所有注释。
@ts-ignore
(这些注释使我们可以告诉编译器忽略下一行发生的错误)。如果我们将用JavaScript编写的项目转换为TypeScript,则此插件将不执行任何操作。但是,如果我们谈论的项目部分是用JS编写的,部分是用TS编写的(我们的几个项目处于类似状态),那么这是第一个不能放弃的迁移步骤。只有在删除注释后,@ts-ignore
TypeScript编译器才会生成需要修复的诊断错误消息。
这是此插件输入中的代码:
const str3 = foo
? // @ts-ignore
// @ts-ignore comment
bar
: baz;
这是输出:
const str3 = foo
? bar
: baz;
删除注释后,
@ts-ignore
我们运行hoistClassStaticsPlugin插件。它遍历所有类声明。该插件检测是否可能产生标识符或表达式,并确定某个赋值操作是否已经提升到类级别。
为了确保较高的开发速度并避免强制降级到项目的先前版本,我们为每个插件和ts-migrate提供了一组单元测试。
与React相关的插件
在此工具之上构建 的reactPropsPlugin将类型信息从PropTypes转换为TypeScript类型声明。使用此插件,您只需要处理包含至少一个React组件的.tsx文件。该插件查找所有PropTypes声明,并尝试使用AST和简单的正则表达式(例如),或使用更复杂的正则表达式(例如/ objectOf $ /)来解析它们。当检测到React-component(功能或基于类)时,它将转换为使用新类型输入参数(props)的组件:。ReactDefaultPropsPlugin 插件
/number/
type Props = {…};
负责在React组件中实现defaultProps模式。我们使用一种特殊类型来表示输入参数,这些参数被赋予默认值:
type Defined<T> = T extends undefined ? never : T;
type WithDefaultProps<P, DP extends Partial<P>> = Omit<P, keyof DP> & {
[K in Extract<keyof DP, keyof P>]:
DP[K] extends Defined<P[K]>
? Defined<P[K]>
: Defined<P[K]> | DP[K];
};
我们尝试查找已分配默认值的道具,然后将它们与描述我们在上一步中创建的组件的道具的类型结合起来。
React生态系统广泛使用状态和组件生命周期的概念。我们将在接下来的两个插件中解决与这些概念相关的挑战。因此,如果组件具有状态,则reactClassStatePlugin插件会生成一个新类型(
type State = any;
),而reactClassLifecycleMethodsPlugin插件会使用相应的类型注释组件的生命周期方法。可以扩展这些插件的功能,包括为其配备以any
更精确的类型替换它们的功能。
可以通过扩展对状态和属性的类型支持来改进这些插件。但是事实证明,它们的现有功能是实现我们所需功能的良好起点。我们这里也不使用React钩子,因为在迁移开始时,我们的代码库使用了不支持钩子的旧版本的React。
检查项目是否正确编译
我们的目标是在不更改程序行为的情况下编译具有基本类型的TypeScript项目。
经过所有的转换和修改后,我们的代码可能被格式化为不一致的格式,这可能导致以下事实:某些带有lint的代码检查会发现错误。我们的前端代码库使用基于Prettier和ESLint的系统。也就是说,更漂亮用于自动代码格式化,并ESLint有助于检查代码是否符合推荐的开发方法。所有这些使我们能够通过使用适当的插件来快速处理由先前操作引起的代码格式问题。-
eslintFixPlugin
。
迁移管道的最后一步是验证是否已解决所有TypeScript编译问题。为了查找和修复潜在的错误,tsIgnorePlugin插件从代码和行号的语义诊断中获取信息,然后在代码中添加注释
@ts-ignore
以及错误说明。例如,它可能看起来像这样:
// @ts-ignore ts-migrate(7053) FIXME: No index signature with a parameter of type 'string...
const { field1, field2, field3 } = DATA[prop];
// @ts-ignore ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const field2 = object.some_property;
我们为系统配备了JSX语法支持:
{*
// @ts-ignore ts-migrate(2339) FIXME: Property 'NORMAL' does not exist on type 'typeof W... */}
<Text weight={WEIGHT.NORMAL}>
some text
</Text>
<input
id="input"
// @ts-ignore ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'string'.
name={getName()}
/>
有了有意义的错误消息供我们使用,可以更轻松地修复错误并查找要查找的代码段。相关注释与结合使用
$TSFixMe
,使我们可以收集有关代码质量的有价值的数据并查找可能有问题的代码片段。$TSFixMe
是我们创建的类型别名any
。对于函数,这是$TSFixMeFunction = (…args: any[]) => any;
。建议避免使用type any
,但使用它有助于我们简化迁移过程。使用这种类型有助于我们确切地知道哪些代码片段需要改进。
值得注意的是,该插件
eslintFixPlugin
运行了两次。使用前第一次tsIgnorePlugin
因为格式化会影响有关编译错误发生位置的消息。第二次是在应用程序之后tsIgnorePlugin
,因为在代码中添加注释@ts-ignore
会导致格式错误。
补充说明
我们想提请您注意我们在工作中注意到的几个迁移功能。也许在您的项目中了解这些功能会很方便。
- TypeScript 3.7 @ts-nocheck, TypeScript- . , .js-, .ts/.tsx-. , .
- TypeScript 3.9引入了对@ ts-expect-error注释的支持。如果一行代码带有这样的注释,TypeScript将不会报告相应的错误。如果这样的行没有错误,TypeScript将通知
@ts-expect-error
您不需要注释。Airbnb代码库已从注释@ts-ignore
变为注释@ts-expect-error
。
结果
Airbnb的代码库从JavaScript到TypeScript的迁移仍在进行中。我们有一些旧项目仍由JavaScript代码表示。
$TSFixMe
注释在我们的代码库中仍然很常见@ts-ignore
。
JavaScript和打字稿中的Airbnb
但要注意的是,使用TS-迁移大大加快把我们的项目从JS到TS的过程,大大提高了我们的工作的生产力。使用ts-migrate,程序员可以专注于改善类型,而不是手动处理每个文件。当前,大约有600万行代码的前端单一存储库中有86%被转换为TypeScript。我们预计到今年年底将达到95%。
在项目存储库主页上,您可以了解如何安装和运行ts-migrate。如果您在ts-migrate中发现任何问题,或者您有改进此工具的想法,我们邀请您加入。努力吧!
您是否曾经将大型项目从JavaScript转换为TypeScript?