Recently, I was transforming a React Native project from JS and TS to full TS. I saw that Airbnb has a tool for migrating to TS, and there is an article describing the use of this tool and the process of migrating to Airbnb, so I translated it here for understanding. The original link

Typescript is the official front-end development language for Airbnb. Currently, it is impossible to migrate a mature code base containing thousands of JavaScript files to Typescript in a single day. At Airbnb, Typescript applications have gone from being proposed, tried and tested by multiple teams, to being the official airbnb front-end development language. You can read Bire Bunge’s talk to see how we’re rolling out Typescript on a massive scale.

Migration strategy

Large-scale migration is a complex task, and we explored several options for converting from JavaScript to TypeScript:

Hybrid compilation strategy

Perform partial migration, file by file, and fix type errors until the entire project is migrated. The allowJS configuration option makes this possible by allowing both JS and TS files to exist in our project.

Under the mixed migration strategy, we don’t have to pause development and can gradually migrate files file by file. But in the big picture, migration can take a long time. There is also the need to educate and bring in other developers within the organization.

Total Migration strategy

A JS project or JS, TS mixed project completely transformed into a pure TS project. We need some type any and the @ts-ignore annotation to make the project run error-free, but in the long run we’ll have to replace them with more descriptive class names.

Choosing a full migration strategy has several obvious advantages:

  • Project consistency: The overall migration strategy ensures that the state of each file is consistent. Developers don’t need to remember where Typescript features can be used and where underlying errors are blocked by the compiler
  • Just adding types is easier than fitting the entire file: a file with a lot of external dependencies is hard to fix alone. In mixed mode, it is difficult to track the repair progress and status of a file. (Translator’s Note: There is not much logic in the original text… If a ts file relies on a JS file, the error will be reported. If you have to add a type to the js file, it will be annoying.

From this point of view, all migration strategies have an overall advantage! However, the actual execution of a full-scale migration of a large, mature code base is a burdensome and complex problem. To solve this problem, we decided to use the script Codemod! Through our initial manual migration to Typescript, we found that some repetitive work could be automated. We made codemod for each step and integrated them into the overall migration pipeline.

In our experience, it is impossible to guarantee 100% error free projects in an automated migration, but we have found that the combination of the following steps allows the project to complete without error. We’ve tried migrating over 5W lines of code and over a thousand files from JavaScript to Typescript in one day based on codemod.

Based on this pipeline, we created a tool called TS-Migrate!

React is an important part of the front-end project at Airbnb, which is why part of Codemod covers the React concept. With additional configuration and testing, TS-Migrate can be used with other frameworks or libraries.

Steps in the migration process

Let’s look at the main steps required to migrate a project from JavaScript to Typescript and how to implement them:

  1. Json files should be created initially for all Typescript projects. Ts-migrate can do this automatically, if necessary, with a default configuration file template and validation checks to ensure that all projects are configured correctly. Here is an example of a basic configuration
{
  "extends": ".. /typescript/tsconfig.base.json"."include": [".".".. /typescript/types"]}Copy the code

2. Once tsconfig.json is configured, the next step is to change the source file name extension from.js/.jsx to.ts/.tsx. This step is very easy to automate and eliminates a significant amount of manual work.

The next step is to run codemod! We call it a plug-in. Ts-migrate plugins are codemods that get additional information from the Typescript server. The plug-in takes a string as input and returns an updated string as output. Jscodeshift, TypeScript API, string substitution, or other AST modification tools can be used for more powerful code transformations.

As a result of these steps, we check Git history and commit lists to see if any code is waiting to be merged. This helps us distinguish the commit that migrates the pull request, making it easier to understand and track file renaming.

An overview of the TS-Migrate package

Ts-migrate has been divided into three packages:

  1. ts-migrate
  2. ts-migrate-server
  3. ts-migrate-plugins

By doing so, we can separate the transformation logic from the core code and add multiple configurations for different purposes. Choice, we have two main configurations: Migration and reignore

When the migration configuration only wants to migrate from JS to TS, reignore allows the project to compile by simply ignoring errors. The Reignore configuration is useful when a large project meets the following requirements:

  • Upgrading the TS Version
  • Major changes to the code base
  • Add types for some commonly used libraries

This way, even if there are some errors that we don’t want to deal with right away, we can migrate the project to TS right away. This makes it easier to upgrade versions of TS and dependent libraries.

The overall configuration running on ts-Migrant-Server consists of the following two parts:

  • TSServer: this section is very much like vscode’s communication between the editor and the language server. A new TS language server is run as a separate process and communicates with development tools using the language protocol.

  • Migration Runner: This part runs and coordinates the Migration process and accepts the following parameters:

interface MigrateParams {
  rootDir: string;          // path to the root directory  
  config: MigrateConfig;    // migration config, including list of       
                            // plugins it contains
  server: TSServer;         // an instance of the TSServer fork
}
Copy the code

And it does the following:

  1. Parsing tsconfig. Json
  2. Create the.ts source file
  3. Send each file to the TS language server for diagnostics. The compiler provides us with three types of diagnostics:semanticDiagnostics.syntacticDiagnostics, and suggestionDiagnostics. Use the compiler’s diagnostic capabilities to locate problematic areas in the source code. Based on the unique diagnostic code and line number, we can distinguish the type of problem and modify the code appropriately.
  4. Run the plug-in for each file. If the plug-in needs to modify the contents of the file, we modify the contents of the source file and notify the TS language server that the file has been changed.

Examples of ts- Migrate – Server can be found in the examples directory or home directory. Ts-migrant-example also contains a simple example of using the plug-in. They fall into three main types:

  • jscodeshit-based
  • TypeScript Abstract Syntax Tree (AST)-based
  • text-based

There are examples in the code repository of how to write simple plug-ins of all three types and use them in conjunction with Tsmigrate-Server. Here is a simple example of a migration pipeline that transforms the following code:

function mult(first, second) {
  return first * second;
}
Copy the code

become

function tlum(tsrif: number, dnoces: number) :number {
  console.log(`args: The ${arguments}`);
  return tsrif * dnoces;
}
Copy the code

For this example, TS-Migrate does three transformations:

  1. Convert all identifiersfirst -> tsrif
  2. Add a type declaration for a functionfunction tlum(tsrif, dnoces) -> function tlum(tsrif: number, dnoces: number): number
  3. insertThe console. The log (' the args: ${the arguments} ");

General plug-in

The actual plug-ins are in a separate NPM package – ts-migrant-plugins. Let’s take a look at the plug-ins. There are two based on jscodeshift – -based plug-in: explicitAnyPlugin and declareMissingClassPropertiesPlugin. Jscodeshift is a tool that converts the AST back to a string through the toSource() function in the Recast package, so we can update the source code directly in all files.

The explicitAnyPlugin works by extracting all semantically diagnosed errors and line numbers from the TS language server. We then need to add the any type to the diagnosed rows. This method allows us to resolve the error because adding any fixes the compilation error.

Previous code

const fn2 = function(p3, p4) {}
const var1 = [];
Copy the code

Next code

const fn2 = function(p3: any, p4: any) {}
const var1: any = [];
Copy the code

DeclareMissingClassPropertiesPlugin performed all code 2339 related diagnosis (can you guess what it means to the code), if you can find the lack of identifier class declaration, the plug-in will give class to add any type declarations. As the name of the plug-in says, it only works with ES6 classes.

The next category of plug-ins are those based on TS AST. By parsing the AST, we can generate an update array of the following types in the source file:

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 };
Copy the code

After generating the update array, the only thing left is to apply the update inversions to the code. By doing this, we can get the new text and then update the source file. Ast-based plug-ins include stripTSIgnorePlugin and hoistClassStaticsPlugin.

StripTSIgnorePlugin is the first plug-in in the migration pipeline. It removes @ts-ignore from all source files. If we’re converting a pure JS project to a TS project, this plug-in won’t do anything. However, if it’s a JS/TS hybrid project (and at Airbnb we have a lot of projects in this state), it’s an essential step. Only by removing @ts-ignore can the TS compiler print all the semantic errors that need to be traced.

const str3 = foo
  ? // @ts-ignore
    // @ts-ignore comment
    bar
  : baz;
Copy the code

Converted to

const str3 = foo
  ? bar
  : baz;
Copy the code

After removing the @ts-ignore comment we run hoistClassStaticsPlugin. The plug-in iterates through all the class declarations in the file. To determine if we can promote identifiers or expressions, and to determine if we have promoted to a class.

In order to iterate quickly and prevent regression, we added a series of unit tests for each plug-in and TS-Migrate.

React related plugins

The reactPropsPlugin converts the type information from the PropTy distribution to the type definition of the TS props. This is based on a great tool written by Mohsen Azimi. We need to run the plugin in a.tsx file that contains at least one React component. ReactPropsPlugin finds all of the PropTypes declarations and tries to parse them with ast and simple regees like /number/ or more complex /objectOf$/. When a React component (whether function or class) is found, it will transform the new component. The new component’s props will have a new type: Type props = {… }

The reactDefaultPropsPlugin overrides the format of defaultProps in the React component. We use a special type to represent props with a default value:

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];
};
Copy the code

We tried to find the default props declarations and merge them with the component props types generated in the previous step.

In the React architecture, the concepts of state and lifecycle are very common. We use two plug-ins to track them. If a component is stateful, the reactClassStatePlugin generates a new type type State = any; With reactClassLifecycleMethodsPlugin props types to declare the function component life cycle. The functionality of both plug-ins can be extended to include more meaningful types instead of any.

There is still room for more enhancements and better type support for state and props. As a start, however, the current functionality is sufficient. We did not override hooks because our code base used older versions of React before the migration.

Ensure successful compilation of the project

Our goal is for TS projects to have basic type coverage without changing runtime behavior. After all the conversions and modifications above, there may be some inconsistent formatting and Lint checking errors in the code. Our front-end code base uses a Prettier-ESLint setting –Prettier is used to format code automatically, and ESLint ensures that code follows best practices. Therefore, we can quickly solve all the formatting problems that might have been introduced by the previous step by running eslint-Prettier in a plug-in.

The last part of the migration pipeline ensures that all TypeScript compilation issues are resolved. To find potential errors, tsignorePlugin performs a semantic diagnosis with a line number and inserts an @TS-ignore comment with a useful explanation such as

// @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;
Copy the code

We also support JSX syntax

{*
// @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()}
/>
Copy the code

Meaningful error messages in comments make it easier to fix problems and view code that needs attention. These comments, in conjunction with TSFixMe, allow us to gather data on code quality and identify blocks of code that may have potential problems.

Last but not least, we need to run the ESlint-fix plugin twice. The formatting of the tsIgnorePlugin may affect the output of the compilation error. After running tsIgnorePlugin, a new formatting error may occur when the @TS-ignore annotation is inserted.

conclusion

Our migration continues: we have some legacy projects that are still JS and a lot of $TSFixMe and @TS-Ignore comments in the code base.

Using TS-Migrate has greatly accelerated our migration process and productivity. Engineers can focus on type promotion rather than manually migrating file by file. Currently, 86% of the front-end MonorePO on 6M-Line has been converted to TS projects, and 95% is planned by the end of the year.

You can pull the source code of TS-Migrate from the Github repository or find instructions for installing and running the migrate in the codebase. If you have any questions or ideas for optimization, you are welcome to contribute to the code base.

Thanks to Brie Bunge, author of TS-Migrate and the driving force behind TS on Airbnb. Thanks to Joe Lencioni for helping us implement TS and build ts infrastructure and tools at Airbnb. Special thanks to Elliot Sachs and John Haytko for their contribution to TS-Migrate. Thanks to everyone who provided feedback and help along the way!

footnotes

We’d like to point out a few things about migration that we discovered during this process that might be useful:

  • Ts version 3.7 introduced the @ts-nocheck annotation, which can be added to the file header to disable syntax checking. We didn’t use this comment because earlier it didn’t support. Ts /.tsx files. But it helps a lot during the migration process.
  • Version 3.9 of TS introduced the @ts-expect-error annotation. If a line of code is preceded by an @ts-expect-error comment, TS does not report that error. If there are no errors, ts reports that the annotation is not needed. At Airbnb we use @ts-expect-error annotations instead of @ts-ignore