jsweet で java を TypeScript に変換してみる

Java

java と TypeScript は構文に類似点が多く、自動変換してくれるソリューションも複数存在します。
この記事では OSS である jsweet を利用し、どのくらい変換できるものなのか試した結果です。

結論から言うと、枠(クラス定/関数定義など) + 一部ロジック程度は変換が効きそうです。ただ、うまく行かないであろう箇所も多々あるのと、jsweet 自体の活動も活発ではなさそうなので、いま時点でなにかに使えるかというと難しいかなという印象です。

※ 今回はほぼ素で試した結果です。オプションを使えばもっとうまくできるかもしれません。

準備

jsweet はこちら https://github.com/cincheo/jsweet

quickstart があるので、こちらを clone します。

$ git clone https://github.com/cincheo/jsweet-quickstart.git

プロジェクトを開いたあと、pom.xml で module の設定を common.js にしておきます。

				<groupId>org.jsweet</groupId>
				<artifactId>jsweet-maven-plugin</artifactId>
				<version>${jsweet.transpiler.version}</version>
				<configuration>
					<verbose>true</verbose>
					<tsOut>target/ts</tsOut>
					<outDir>target/js</outDir>
					<candiesJsOut>webapp/candies</candiesJsOut>
					<targetVersion>ES6</targetVersion>
					<module>commonjs</module>
					<moduleResolution>classic</moduleResolution>
				</configuration>

あとは mvn generate-sources をするのみです。ちなみに java 11 です。

以降、何パターン化変換してみた結果。
どんな形に変換されるかが目的なので実装は適当です(無視してください)。

public / private / final / static

元コード

public class JSweetHello {
    private final String foo = "finalString";

    public int publicMethod1(int a, int b) {
        return a + b;
    }

    private int privateMethod1(int a, int b) {
        return a + b;
    }

    public static int staticPublicMethod1(int a, int b) {
        return a + b;
    }

    public int publicMethod2(int a, String b, Long c, Float d, ArrayList<String> e, TestModel f) {
        return 10;
    }
}

変換後

  • public は付与される、private は除去された形に変換されるようです。
  • static メソッドは付与されたまま
  • final はコンストラクタで設定される
/* Generated from Java with JSweet 3.1.0 - http://www.jsweet.org */
export class JSweetHello {
    /*private*/ foo: string;

    public publicMethod1(a: number, b: number): number {
        return a + b;
    }

    privateMethod1(a: number, b: number): number {
        return a + b;
    }

    public static staticPublicMethod1(a: number, b: number): number {
        return a + b;
    }

    public publicMethod2(a: number, b: string, c: number, d: number, e: Array<string>, f: JSweetHello.TestModel): number {
        return 10;
    }

    constructor() {
        this.foo = "finalString";
    }
}
JSweetHello["__class"] = "quickstart.JSweetHello";

型(基本、配列)クラス

元コード

package quickstart;

import java.lang.reflect.Array;
import java.util.ArrayList;

public class JSweetHello {
    private class TestModel {
        int var1;
        String var2;
        Long var3;
        Float var4;
        ArrayList<String> var5;

        TestModel(int a, String b, Long c, Float d, ArrayList<String> e) {
            this.var1 = a;
            this.var2 = b;
            this.var3 = c;
            this.var4 = d;
            this.var5 = e;
        }
    }

    public int publicMethod2(int a, String b, Long c, Float d, ArrayList<String> e, TestModel f) {
        return 10;
    }
}

変換後

  • namespace が追加され、その中に該当のクラスが変換された形で含まれています。
  • _parent というプロパティが追加になっています。
  • undefined の場合、null を設定していますが、型定義的には手動で修正が必要
export namespace JSweetHello {
    export class TestModel {
        public __parent: any;
        var1: number;
        var2: string;
        var3: number;
        var4: number;
        var5: Array<string>;

        constructor(__parent: any, a: number, b: string, c: number, d: number, e: Array<string>) {
            this.__parent = __parent;
            if (this.var1 === undefined) { this.var1 = 0; }
            if (this.var2 === undefined) { this.var2 = null; }
            if (this.var3 === undefined) { this.var3 = null; }
            if (this.var4 === undefined) { this.var4 = null; }
            if (this.var5 === undefined) { this.var5 = null; }
            this.var1 = a;
            this.var2 = b;
            this.var3 = c;
            this.var4 = d;
            this.var5 = e;
        }
    }
    TestModel["__class"] = "quickstart.JSweetHello.TestModel";
}

interface

元コード

package quickstart;

public interface TestInterface {
    public int interfaceMethod();

    public default String interfaceMethod2() {
        return "hoge";
    }
}
package quickstart;

public class TestImplement implements TestInterface{
    public int interfaceMethod() {
        return 100;
    }
}

変換後

  • interface のみになり、デフォルト実装は消える・
/* Generated from Java with JSweet 3.1.0 - http://www.jsweet.org */
export interface TestInterface {
    interfaceMethod(): number;

    interfaceMethod2(): string;
}
  • interface の default 実装は利用しているクラス側でちゃんと実装される。
  • import 文もちゃんとつく
/* Generated from Java with JSweet 3.1.0 - http://www.jsweet.org */
export class TestImplement implements TestInterface {
    /* Default method injected from quickstart.TestInterface */
    public interfaceMethod2(): string {
        return "hoge";
    }
    public interfaceMethod(): number {
        return 100;
    }

    constructor() {
    }
}
TestImplement["__class"] = "quickstart.TestImplement";
TestImplement["__interfaces"] = ["quickstart.TestInterface"];

import { TestInterface } from './TestInterface';

例外

元コード

Java 標準の例外クラスとカスタムのクラスを試す。

package quickstart;

public class CustomException extends Exception {
    CustomException(String msg) {
        super(msg);
    }
}
package quickstart;

public class TestException {
    public int exceptionMethod(int num1, int num2) throws ArithmeticException {
        try {
            int result = num1 / num2;
            if (result == 0) {
                throw new CustomException("custom error");
            }
            return result;
        } catch (ArithmeticException e) {
            throw e;
        } catch (CustomException e) {
            System.out.println("custom error");
        } finally {
            System.out.println("hoge");
        }
        return num1;
    }
}

変換後

  • 多少要らない記述はありますが、変換はできてそうです(<any>Object は any へのキャスト)。
  • 継承もそのまま残っています。Javascript 標準の Error クラスの継承に置き換わっており、prototype を自身のクラスに設定しています。
/* Generated from Java with JSweet 3.1.0 - http://www.jsweet.org */
export class CustomException extends Error {
    constructor(msg: string) {
        super(msg);
        this.message=msg;
        (<any>Object).setPrototypeOf(this, CustomException.prototype);
    }
}
CustomException["__class"] = "quickstart.CustomException";
  • import もちゃんとされています
  • System.out.printlnconsole.info に変換されています。
  • java 標準の例外は全体的に見直しが必要そう。
/* Generated from Java with JSweet 3.1.0 - http://www.jsweet.org */
export class TestException {
    public exceptionMethod(num1: number, num2: number): number {
        try {
            const result: number = (num1 / num2|0);
            if (result === 0){
                throw new CustomException("custom error");
            }
            return result;
        } catch(__e) {
            if(__e != null && (__e["__classes"] && __e["__classes"].indexOf("java.lang.ArithmeticException") >= 0)) {
                const e: Error = <Error>__e;
                throw e;

            }
            if(__e != null && __e instanceof <any>CustomException) {
                const e: CustomException = <CustomException>__e;
                console.info("custom error");

            }
        } finally {
            console.info("hoge");
        }
        return num1;
    }
}
TestException["__class"] = "quickstart.TestException";

import { CustomException } from './CustomException';

その他

もう少し複雑なものも試したい、ということで chatGPT に作ってもらいました。動作確認はしていませんが、以下とのこと。

  • removeDuplicates メソッドは整数の配列から重複を削除
  • reverseArray メソッドは、整数の配列を逆順
  • arrayToString メソッドは、整数の配列を文字列に変換
import java.util.Arrays;
import java.util.stream.Collectors;

public class ArrayOperations {

    public static int[] removeDuplicates(int[] arr) {
        return Arrays.stream(arr).distinct().toArray();
    }

    public static int[] reverseArray(int[] arr) {
        int[] reversed = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            reversed[i] = arr[arr.length - i - 1];
        }
        return reversed;
    }

    public static String arrayToString(int[] arr) {
        return Arrays.stream(arr)
                .mapToObj(Integer::toString)
                .collect(Collectors.joining(", "));
    }

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 2, 4, 5, 3, 6};
        System.out.println("Original array: " + arrayToString(arr));
        int[] uniqueArr = removeDuplicates(arr);
        System.out.println("Array with duplicates removed: " + arrayToString(uniqueArr));
        int[] reversedArr = reverseArray(arr);
        System.out.println("Reversed array: " + arrayToString(reversedArr));
    }
}

変換後

  • Arrays が処理できませんでした。こちら にある方法も試しましたがエラーが増え結局うまく通すことはできませんでした。
  • reverseArray も前半の処理は期待してないものになっているようです。
/* Generated from Java with JSweet 3.1.0 - http://www.jsweet.org */
export class ArrayOperations {
  public static removeDuplicates(arr: number[]): number[] {
    return Arrays.stream(arr).distinct().toArray();
  }

  public static reverseArray(arr: number[]): number[] {
    const reversed: number[] = ((s) => {
      let a = [];
      while (s-- > 0) a.push(0);
      return a;
    })(arr.length);
    for (let i: number = 0; i < arr.length; i++) {
      {
        reversed[i] = arr[arr.length - i - 1];
      }
    }
    return reversed;
  }

  public static arrayToString(arr: number[]): string {
    return <any>Arrays.stream(arr)
      .mapToObj<any>((arg0) => {
        return Number.toString(arg0);
      })
      .collect<any, any>(Collectors.joining(", "));
  }

  public static main(args: string[]) {
    const arr: number[] = [1, 2, 3, 2, 4, 5, 3, 6];
    console.info("Original array: " + ArrayOperations.arrayToString(arr));
    const uniqueArr: number[] = ArrayOperations.removeDuplicates(arr);
    console.info(
      "Array with duplicates removed: " +
        ArrayOperations.arrayToString(uniqueArr)
    );
    const reversedArr: number[] = ArrayOperations.reverseArray(arr);
    console.info(
      "Reversed array: " + ArrayOperations.arrayToString(reversedArr)
    );
  }
}
ArrayOperations["__class"] = "quickstart.ArrayOperations";

ArrayOperations.main(null);

考察

プログラム構造(クラス/関数定義、引数の型の指定の仕方など)はいい感じにけそうです。

abstruct や アノテーションをつけた場合などまだ試していないことはありますが、java 早期から使えるような構文で純粋なロジック部分についてはある程度変換できるかな、という感触です。が、うまく行かないところも多々あるので、その点もし利用する際は注意が必要かと思います。