ts-expect-error を付与しながら .js を .ts に一括で書き換える

typescriptby mizchi created at 2020/06/23/20:52

ToC

やりたいこと

巨大なコードベースに対して、 .js をとりあえず .ts に書き換えてしまいたい。

だが、素朴な拡張子の書き換えで型違反が出ると jest やその他ツールが止まりはじめて面倒。

なので、エラー行には // @ts-expect-error を自動で付与しながら書き換えたい。@ts-ignore ではなく、@ts-expect-error なのは、型エラーを修正した際に、それが伝搬するようにするため。

方法

  • ファイルの拡張子を .js から .ts に書き換える
  • typescript service 経由で型を検査し、エラー行を集める
  • エラーが出た行に // @ts-expect-error を付与する

コード

書き捨てなので汚いです

import * as ts from "typescript";
import { uniq } from "lodash";
import fs from "fs";
import path from "path";

type ErrorLinesMap = { [k: string]: number[] };

function rewriteToTS(files: string[]) {
  files.map((f) => {
    fs.renameSync(f, f.replace(/\.js$/, ".ts"));
    console.log("rename", f, f.replace(/\.js$/, ".ts"));
  });
}

function compile(
  tsFileNames: string[],
  options: ts.CompilerOptions
): ErrorLinesMap {
  const program = ts.createProgram(tsFileNames, options);
  // skip write file. only diagnostics
  const emitResult = program.emit(undefined, () => {});

  const allDiagnostics = ts
    .getPreEmitDiagnostics(program)
    .concat(emitResult.diagnostics);

  const errorLines: ErrorLinesMap = {};
  allDiagnostics.forEach((diagnostic) => {
    if (diagnostic.file) {
      let { line } = diagnostic.file.getLineAndCharacterOfPosition(
        diagnostic.start!
      );
      let last = errorLines[diagnostic.file.fileName];
      if (last == null) {
        last = errorLines[diagnostic.file.fileName] = [];
      }
      errorLines[diagnostic.file.fileName] = uniq([...last, line]);
    }
  });
  return errorLines;
}

function rewriteFiles(errorLines: ErrorLinesMap) {
  Object.entries(errorLines).map(([file, errors]) => {
    const content = fs.readFileSync(file, "utf-8");
    const lines = content.split("\n");
    const newLines = lines.map((line, idx) => {
      const m = line.match(/^\s*/);
      let head = "";
      if (m) {
        head = m[0];
      }
      if (errors.includes(idx)) {
        return `${head}// @ts-expect-error\n${line}`;
      } else {
        return line;
      }
    });
    // dry run if you need
    const out = newLines.join("\n");
    fs.writeFileSync(file, out);
    console.log("update", file);
  });
}

const configPath = path.join(process.cwd(), "tsconfig.json");
const tsconfig = ts.parseConfigFileTextToJson(
  configPath,
  fs.readFileSync(configPath, "utf-8")
);

const files = process.argv.slice(2);
rewriteToTS(files);
const tsFiles = files.map((f) => f.replace(".js", ".ts"));
const errors = compile(tsFiles, tsconfig.config!);
rewriteFiles(errors);

使い方

# yarn add ts-node typescript lodash -D
yarn ts-node -T -O '{"module":"commonjs"}' add-expect-error.ts src/*.js

こういう変換を想定

// src/foo.js
const x = {};
x.foo = 1;
// src/foo.ts
const x = {};
// @ts-expect-error
x.foo = 1;

注意点

  • tsconfig.json があるディレクトリで実行してください
  • transform 手順によりますが、ts の変換は babel と似た出力をするはずですが、完全に同一ではないです

雑なので色々動かないことはあると思いますが、一旦はこれで。

History
085ac04 - Update Tue Jun 23 22:11:13 2020 +0900 
68e5f92 - Update Tue Jun 23 21:06:26 2020 +0900