🏫

TS のクラスを型とその関数に変換するコンバーターを書いた

2023/06/07に公開2

https://twitter.com/mizchi/status/1666269359747248130

というわけでシュッと作ってみました。

$ 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 変形です。

https://github.com/mizchi/optools/blob/main/declass/src/transformer.mts

こういうコードを TDD するの、進んでる感があって楽しいですよね。

おわり

Discussion

mizchimizchi

クラスメソッドやスタティクメソッド内の new class ... を想定してなかった。あとで追加する。