CLOVER🍀

That was when it all began.

TypeScriptファイルをJavaScriptファイルに変換したい

これは、なにをしたくて書いたもの?

ひとつ前に、こんなエントリーを書きました。

TypeScriptコードを直接実行したい(ts-node、esbuild-register、tsx) - CLOVER🍀

じゃあ、こういう用途で作成したTypeScriptファイルを、人が読めるようなJavaScriptファイルに変換したくなったらどうするのか…?
ということでちょっと遊んでみました。

TypeScriptはJavaScriptにトランスパイルされるもの、という事実はさておき。

きっかけ

そもそもお題が「そんなこと気にするの?」ですが、ふと調べてみるとTypeScriptをオンラインでJavaScriptに変換するサイトがありまして。

TypeScript to plain JavaScript

これ、どうやっているんだろう?と思って見てみるとBabel(@babel/plugin-transform-typescript)を使っていました。

https://github.com/ritz078/transform/blob/master/pages/api/typescript-to-javascript.ts

割とキレイなJavaScriptに変換されていたので、他の方法と比べてどうなんだろう?と思って試してみようと思ったのがきっかけです。

お題

使う対象は、以下とします。

  • tsc
  • Babel CLI+@babel/plugin-transform-typescript
  • esbuild

esbuildは完全にオマケです。この中で型チェックを行うのはtscのみです。

いずれも、npmモジュールのインストールだけして、設定ファイルは作らないようにします。
多少、オプションで出力結果を調整するのは許容、にします…。

なお、結論を言うとtscでいいかな、と思いました…。

変換する対象は、以下の2パターンにします。

単一のファイル。

index.ts

/**
 * get message
 * @param word word
 * @returns message
 */
function getMessage(word: string): string {
  return `Hello ${word}!!`;
}

// call function
const message: string = getMessage('TypeScript');
// print
console.log(message);

ディレクトリ内の複数のファイル。

src/index.ts

// import
import { getMessage } from './sub/func';

// call function
const message: string = getMessage(`TypeScript`);
// print
console.log(message);

src/sub/func.ts

/**
 * get message
 * @param word word
 * @returns message
 */
export function getMessage(word: string): string {
  return `Hello ${word}!!`;
}

なんとなく、コメントも入れています。このあたりを含めて、どうなるか見ていこうかなと。

環境

今回の環境は、こちら。

$ node --version
v18.18.2


$ npm --version
9.8.1

tsc

最初はtscから。

インストール。

$ npm i -D typescript

バージョン。

  "devDependencies": {
    "typescript": "^5.3.2"
  }

単一のファイルの変換。

$ npx tsc --target esnext --module commonjs index.ts

結果。

index.js

/**
 * get message
 * @param word word
 * @returns message
 */
function getMessage(word) {
    return `Hello ${word}!!`;
}
// call function
const message = getMessage('TypeScript');
// print
console.log(message);

実行。

$ node index.js
Hello TypeScript!!

ディレクトリごと変換。tsconfig.jsonを書かない状態だとできない感じだったので、find+xargsにしました。

$ find src -name '*.ts' | xargs npx tsc --target esnext --module commonjs --outDir dist

結果。

dist/index.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// import
const func_1 = require("./sub/func");
// call function
const message = (0, func_1.getMessage)(`TypeScript`);
// print
console.log(message);

dist/sub/func.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMessage = void 0;
/**
 * get message
 * @param word word
 * @returns message
 */
function getMessage(word) {
    return `Hello ${word}!!`;
}
exports.getMessage = getMessage;

実行。

$ node dist/index.js
Hello TypeScript!!

コメントも残るんですね。ちょっと驚きました。

以降は、実行結果は省略します。

Babel CLI+@babel/plugin-transform-typescript

続いて、Babel CLI+@babel/plugin-transform-typescript。

インストール。

$ npm i -D @babel/core @babel/cli @babel/plugin-transform-typescript

バージョン。

  "devDependencies": {
    "@babel/cli": "^7.23.4",
    "@babel/core": "^7.23.3",
    "@babel/plugin-transform-typescript": "^7.23.4"
  }

単一ファイルの変換。

$ npx babel --plugins @babel/plugin-transform-typescript index.ts --out-file index.js

結果。

index.js

/**
 * get message
 * @param word word
 * @returns message
 */
function getMessage(word) {
  return `Hello ${word}!!`;
}

// call function
const message = getMessage('TypeScript');
// print
console.log(message);

ディレクトリごと変換。

$ npx babel --plugins @babel/plugin-transform-typescript src --out-dir dist
Successfully compiled 2 files with Babel (158ms).

dist/index.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
var func_1 = require("./sub/func");
var message = (0, func_1.getMessage)("TypeScript");

dist/sub/func.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getMessage = void 0;
function getMessage(word) {
  return "Hello ".concat(word, "!!");
}

複数ファイルにすると、一気に挙動が変わりましたね。コメントが失われ、構文がES5相当になりました。

というか、単一ファイルの変換の方が特殊そうですね…。

esbuild

最後はオマケでesbuildです。esbuildはバンドラーなのでここに並べるのはちょっと違うと思いますが、個人的な興味で動かしてみました。

インストール。

$ npm i -D esbuild

バージョン。

  "devDependencies": {
    "esbuild": "^0.19.7"
  }

単一ファイルの変換。

$ npx esbuild index.ts --bundle --platform=node --target=node18 --outfile=index.js

  index.js  132b

⚡ Done in 2ms

結果。

index.js

// index.ts
function getMessage(word) {
  return `Hello ${word}!!`;
}
var message = getMessage("TypeScript");
console.log(message);

ディレクトリごと変換。といっても、1ファイルにまとめられますが。

$ npx esbuild ./src --bundle --platform=node --target=node18 --outfile=dist/index.js

  dist/index.js  156b

⚡ Done in 3ms

結果。

dist/index.js

// src/sub/func.ts
function getMessage(word) {
  return `Hello ${word}!!`;
}

// src/index.ts
var message = getMessage(`TypeScript`);
console.log(message);

構文がES5になりますね…。

ちなみに、出力先をディレクトリにもできますが、その場合は出力結果が入力ディレクトリ(もしくはファイル)の名前になりました。

$ npx esbuild ./src --bundle --platform=node --target=node18 --outdir=dist

  dist/src.js  156b

⚡ Done in 3ms

あと、ちょっとハマったのはディレクトリを指定する場合は./を最初につけないと失敗しますね。

$ npx esbuild src --bundle --platform=node --target=node18 --outfile=dist/index.js
✘ [ERROR] Could not resolve "src"

  Use the relative path "./src" to reference the file "src/index.ts". Without the leading "./", the
  path "src" is being interpreted as a package path instead.

1 error

おわりに

思ったはずみにちょっと試してみましたが、生成されたJavaScriptファイルに対してソースコードして向き合うなら、やっぱりふつうに
書いた方がいいな、と思いました…。

使ったとしても、tscでの変換でしょうね。

ちょっとした興味からのお題でした。