🏫
TS のクラスを型とその関数に変換するコンバーターを書いた
というわけでシュッと作ってみました。
$ npm install @mizchi/declass
$ npx declass input.ts # -o output.ts
何をするか
export class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
console.log("Point created", x, y);
}
distance(other: Point) {
return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
}
}
export class Point3d {
constructor(public x: number, public y: number, public z: number) {}
}
export class Complex {
static staticV: number = 1;
static staticFuncA(){
this.staticFuncB();
};
static staticFuncB(){
console.log('called');
};
_v: number = 1;
get v(): number {
this._v;
};
set v(value: number) {
this._v = value;
};
// no constructor
}
Output
export type Point = {
x: number;
y: number;
};
export function Point$new(x: number, y: number): Point {
const self: Point = { x: x, y: y };
console.log("Point created", x, y);
return self;
}
export function Point$distance(self: Point, other: Point) {
return Math.sqrt(Math.pow(self.x - other.x, 2) + Math.pow(self.y - other.y, 2));
}
export type Point3d = {
x: number;
y: number;
z: number;
};
export function Point3d$new(x: number, y: number, z: number): Point3d { const self: Point3d = { x: x, y: y, z: z }; return self; }
export const Complex$static$staticV: number = 1;
export function Complex$static$staticFuncA() {
Complex$static$staticFuncB();
}
export function Complex$static$staticFuncB() {
console.log("called");
}
export type Complex = {
get v(): number;
set v(value: number);
_v: number;
};
export function Complex$new(): Complex {
const self: Complex = {
get v(): number {
return this._v;
},
set v(value: number) {
this._v = value;
},
_v: 1,
};
return self;
}
なぜ
ESM 環境ではファイルスコープが十分に名前区間として機能しているので、あえて古の Java のように関数がクラスに属する必要が少ないです。
また、JavaScript の class は静的解析ツールにとっても最適化がむずかしいです。クラスの内部スコープを共有するので、どの関数同士が影響があるのかのコールグラフを作るのが難しくなります。
この変換器は rust 風のオブジェクトとメソッドに変換することで、ESM にとって優しいコードを例示するのが目的です。 declass で実行した各実装コードは個別に完全に切り離されているので、これを元に書き換えることで rollup や webpack の treeshake で完全な不要コード削除が機能します。
注意点として、完全な 1:1 変換を行うものではなく、リファクタリングの助けとなることを意図しています。static
や関数同士の相互呼び出しが適当だったり、名前空間の衝突を考慮していないです。
そもそも単体ファイルを解析しているだけなので、これによる影響、例えばインスタンスメソッド相当の呼び出しを手動で import して修正する必要があるでしょう。
実装
TypeScript Transformer として作ったので、自分でコード変形部分だけ使うこともできます。
import ts from "typescript";
import {declassTransformerFactory} from "@mizchi/declass";
const code = `class X {}`;
const source = ts.createSourceFile(
"input.ts",
code,
ts.ScriptTarget.ES2019,
true,
);
const transformed = ts.transform(source, [
declassTransformerFactory,
]);
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
});
const result = printer.printFile(
transformed.transformed[0],
);
中は泥臭い AST 変形です。
こういうコードを TDD するの、進んでる感があって楽しいですよね。
おわり
Discussion
クラスメソッドやスタティクメソッド内の new class ... を想定してなかった。あとで追加する。
0.2.1 で実装した