Embedding TypeScript in JVM applications enables Java developers to leverage TypeScript's type safety and modern JavaScript features while maintaining the robustness of the JVM ecosystem. Here's a comprehensive guide to various approaches.
Approaches Overview
- GraalVM Polyglot - Direct TypeScript execution
- Nashorn with TypeScript Compilation - Legacy approach
- Node.js Process Integration - Most practical approach
- TypeScript to Java Transpilation - Build-time solution
Approach 1: GraalVM Polyglot (Recommended)
Example 1: Basic GraalVM TypeScript Execution
import org.graalvm.polyglot.*; import org.graalvm.polyglot.proxy.*; public class GraalVMTypeScript { public static void main(String[] args) { try (Context context = Context.newBuilder("js") .allowExperimentalOptions(true) .option("js.ecmascript-version", "2022") .allowAllAccess(true) .build()) { // Execute simple TypeScript (as JavaScript) String tsCode = """ let message: string = "Hello from TypeScript!"; let count: number = 42; console.log(message + " Count: " + count); count * 2; """; Value result = context.eval("js", tsCode); System.out.println("Result: " + result.asInt()); } catch (PolyglotException e) { System.err.println("Execution failed: " + e.getMessage()); } } } Example 2: Advanced GraalVM with TypeScript Features
import org.graalvm.polyglot.*; import java.util.Map; import java.util.HashMap; public class AdvancedGraalVMTypeScript { public static void main(String[] args) { try (Context context = Context.newBuilder("js") .allowAllAccess(true) .build()) { // Register Java objects as TypeScript variables Map<String, Object> userData = new HashMap<>(); userData.put("name", "John Doe"); userData.put("age", 30); context.getBindings("js").putMember("userData", userData); context.getBindings("js").putMember("javaSystem", System.class); // Complex TypeScript code with interfaces String complexTsCode = """ // TypeScript interface interface User { name: string; age: number; } // Using Java data const user: User = userData; console.log(`User: ${user.name}, Age: ${user.age}`); // Function with types function processUser(user: User): string { return `Processed: ${user.name.toUpperCase()}`; } // Call function const result = processUser(user); result; """; Value result = context.eval("js", complexTsCode); System.out.println("TypeScript result: " + result.asString()); // Call TypeScript function from Java Value tsFunction = context.eval("js", """ (function(x: number, y: number): number { return x * y + 10; }) """); Value functionResult = tsFunction.execute(5, 3); System.out.println("Function result: " + functionResult.asInt()); } catch (Exception e) { e.printStackTrace(); } } } Approach 2: Node.js Process Integration (Most Practical)
Example 3: TypeScript Execution via Node.js
import java.io.*; import java.nio.file.*; import java.util.concurrent.*; public class NodeTypeScriptExecutor { private final String nodePath; private final String tsNodePath; public NodeTypeScriptExecutor() { this.nodePath = "node"; // Or full path to node executable this.tsNodePath = "npx"; // Use npx to run ts-node } public String executeTypeScript(String typescriptCode) throws Exception { // Create temporary TypeScript file Path tempDir = Files.createTempDirectory("typescript"); Path tsFile = tempDir.resolve("script.ts"); Files.write(tsFile, typescriptCode.getBytes()); try { // Execute using ts-node ProcessBuilder pb = new ProcessBuilder( tsNodePath, "ts-node", tsFile.toAbsolutePath().toString() ); pb.redirectErrorStream(true); Process process = pb.start(); // Read output BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream()) ); StringBuilder output = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } int exitCode = process.waitFor(); if (exitCode != 0) { throw new RuntimeException("TypeScript execution failed: " + output); } return output.toString(); } finally { // Cleanup Files.deleteIfExists(tsFile); Files.deleteIfExists(tempDir); } } public CompletableFuture<String> executeTypeScriptAsync(String typescriptCode) { return CompletableFuture.supplyAsync(() -> { try { return executeTypeScript(typescriptCode); } catch (Exception e) { throw new CompletionException(e); } }); } public static void main(String[] args) throws Exception { NodeTypeScriptExecutor executor = new NodeTypeScriptExecutor(); String tsCode = """ interface Calculator { add(a: number, b: number): number; } class BasicCalculator implements Calculator { add(a: number, b: number): number { return a + b; } } const calc: Calculator = new BasicCalculator(); const result = calc.add(15, 27); console.log("Result:", result); """; String output = executor.executeTypeScript(tsCode); System.out.println("TypeScript Output:\n" + output); } } Example 4: Bidirectional Communication with Node.js
import java.io.*; import java.net.*; import java.nio.file.*; import java.util.*; import java.util.concurrent.*; public class TypeScriptServer { private Process nodeProcess; private ServerSocket serverSocket; private ExecutorService executor; public void startTypeScriptContext(int port) throws Exception { executor = Executors.newFixedThreadPool(2); // Create TypeScript server script String serverScript = """ const net = require('net'); interface Request { id: string; type: string; data: any; } interface Response { id: string; result?: any; error?: string; } const server = net.createServer((socket) => { console.log('Java client connected'); socket.on('data', (data) => { try { const request: Request = JSON.parse(data.toString()); console.log('Received request:', request); let response: Response; switch (request.type) { case 'calculate': const result = evaluateExpression(request.data.expression); response = { id: request.id, result: result }; break; case 'transform': const transformed = transformData(request.data); response = { id: request.id, result: transformed }; break; default: response = { id: request.id, error: 'Unknown request type' }; } socket.write(JSON.stringify(response) + '\\n'); } catch (error) { const errorResponse: Response = { id: 'unknown', error: error.message }; socket.write(JSON.stringify(errorResponse) + '\\n'); } }); socket.on('end', () => { console.log('Java client disconnected'); }); }); function evaluateExpression(expr: string): number { // Safe evaluation - in real scenario, use proper expression evaluator const cleanExpr = expr.replace(/[^0-9+\\-*/().]/g, ''); return eval(cleanExpr); } function transformData(data: any): any { // Example transformation return { ...data, processed: true, timestamp: new Date().toISOString(), transformedBy: 'TypeScript' }; } server.listen(%d, () => { console.log('TypeScript server listening on port %d'); }); """.formatted(port, port); Path scriptFile = Files.createTempFile("ts-server", ".ts"); Files.write(scriptFile, serverScript.getBytes()); // Start Node.js process with ts-node ProcessBuilder pb = new ProcessBuilder("npx", "ts-node", scriptFile.toString()); pb.redirectError(ProcessBuilder.Redirect.INHERIT); nodeProcess = pb.start(); // Wait for server to start Thread.sleep(2000); } public String sendRequest(String type, Object data) throws Exception { try (Socket socket = new Socket("localhost", 8080); PrintWriter out = new PrintWriter(socket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { String requestId = UUID.randomUUID().toString(); Map<String, Object> request = Map.of( "id", requestId, "type", type, "data", data ); out.println(new org.json.JSONObject(request).toString()); return in.readLine(); } } public void shutdown() { if (nodeProcess != null) { nodeProcess.destroy(); } if (executor != null) { executor.shutdown(); } } public static void main(String[] args) throws Exception { TypeScriptServer server = new TypeScriptServer(); try { server.startTypeScriptContext(8080); // Test calculations String calcResponse = server.sendRequest("calculate", Map.of("expression", "10 * 5 + 2")); System.out.println("Calculation result: " + calcResponse); // Test data transformation String transformResponse = server.sendRequest("transform", Map.of("name", "John", "value", 100)); System.out.println("Transformation result: " + transformResponse); } finally { server.shutdown(); } } } Approach 3: TypeScript to Java Transpilation
Example 5: Build-time TypeScript to Java Conversion
import javax.tools.*; import java.io.*; import java.nio.file.*; import java.util.*; public class TypeScriptToJavaTranspiler { public static class SimpleTranspiler { public String transpileTypeScriptToJava(String tsCode) { // Simple regex-based transpilation for demonstration // In real scenario, use proper TypeScript compiler API String javaCode = tsCode // Convert TypeScript types to Java .replaceAll("let\\s+(\\w+)\\s*:\\s*string\\s*=", "String $1 =") .replaceAll("let\\s+(\\w+)\\s*:\\s*number\\s*=", "int $1 =") .replaceAll("let\\s+(\\w+)\\s*:\\s*boolean\\s*=", "boolean $1 =") .replaceAll("const\\s+(\\w+)\\s*:\\s*string\\s*=", "final String $1 =") .replaceAll("function\\s+(\\w+)\\(([^)]*)\\)\\s*:\\s*(\\w+)", "public static $3 $1($2)") // Remove TypeScript-specific syntax .replaceAll":\\s*\\w+", "") // Remove remaining type annotations .replaceAll("console\\.log", "System.out.println"); return wrapInJavaClass(javaCode); } private String wrapInJavaClass(String code) { return """ import java.util.*; public class TranspiledTypeScript { %s public static void main(String[] args) { // Entry point for transpiled code executeTranspiledLogic(); } public static void executeTranspiledLogic() { %s } } """.formatted(extractMethods(code), extractLogic(code)); } private String extractMethods(String code) { // Extract method definitions (simplified) StringBuilder methods = new StringBuilder(); String[] lines = code.split("\n"); for (String line : lines) { if (line.trim().startsWith("public static")) { methods.append(line).append("\n"); } } return methods.toString(); } private String extractLogic(String code) { // Extract executable logic (simplified) StringBuilder logic = new StringBuilder(); String[] lines = code.split("\n"); for (String line : lines) { if (!line.trim().startsWith("public static") && !line.trim().startsWith("interface") && !line.trim().startsWith("class")) { logic.append(line).append("\n"); } } return logic.toString(); } } public static void compileAndExecute(String javaCode) throws Exception { // Save to temporary file Path tempFile = Files.createTempFile("TranspiledTypeScript", ".java"); Files.write(tempFile, javaCode.getBytes()); // Compile using Java Compiler API JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>(); try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) { Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(tempFile); List<String> options = Arrays.asList("-d", tempFile.getParent().toAbsolutePath().toString()); JavaCompiler.CompilationTask task = compiler.getTask( null, fileManager, diagnostics, options, null, compilationUnits); boolean success = task.call(); if (success) { // Load and execute URLClassLoader classLoader = new URLClassLoader( new URL[]{tempFile.getParent().toFile().toURI().toURL()}); Class<?> clazz = classLoader.loadClass("TranspiledTypeScript"); clazz.getMethod("main", String[].class) .invoke(null, (Object) new String[0]); } else { for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) { System.err.format("Error on line %d: %s%n", diagnostic.getLineNumber(), diagnostic.getMessage(null)); } } } finally { Files.deleteIfExists(tempFile); } } public static void main(String[] args) throws Exception { SimpleTranspiler transpiler = new SimpleTranspiler(); String typeScriptCode = """ let message: string = "Hello from transpiled TypeScript!"; let count: number = 42; function multiply(a: number, b: number): number { return a * b; } console.log(message); let result = multiply(count, 2); console.log("Result: " + result); """; String javaCode = transpiler.transpileTypeScriptToJava(typeScriptCode); System.out.println("Transpiled Java code:\n" + javaCode); // Compile and execute compileAndExecute(javaCode); } } Approach 4: Advanced Integration with TypeScript Compiler API
Example 6: Using TypeScript Compiler Programmatically
import javax.script.*; import java.io.*; import java.nio.file.*; import java.util.*; public class TypeScriptCompilerIntegration { private final ScriptEngine engine; public TypeScriptCompilerIntegration() throws Exception { // Initialize Nashorn (for JavaScript execution) engine = new ScriptEngineManager().getEngineByName("nashorn"); // Load TypeScript compiler loadTypeScriptCompiler(); } private void loadTypeScriptCompiler() throws Exception { // Load TypeScript compiler JavaScript // Note: This requires typescript.js to be available String compilerPath = "node_modules/typescript/lib/typescript.js"; if (Files.exists(Paths.get(compilerPath))) { String compilerCode = Files.readString(Paths.get(compilerPath)); engine.eval(compilerCode); } else { // Fallback: Load from classpath or download System.err.println("TypeScript compiler not found at: " + compilerPath); } } public String compileTypeScript(String tsCode) throws ScriptException { // Use TypeScript compiler to transpile to JavaScript String compileScript = """ var ts = typescript; var sourceCode = `%s`; var result = ts.transpileModule(sourceCode, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS, strict: true } }); result.outputText; """.formatted(tsCode.replace("`", "\\`")); return (String) engine.eval(compileScript); } public Object executeTypeScript(String tsCode) throws Exception { String jsCode = compileTypeScript(tsCode); return engine.eval(jsCode); } public static void main(String[] args) throws Exception { TypeScriptCompilerIntegration integration = new TypeScriptCompilerIntegration(); String typeScriptCode = """ interface Person { name: string; age: number; } function greet(person: Person): string { return `Hello, ${person.name}! You are ${person.age} years old.`; } const user: Person = { name: "Alice", age: 25 }; greet(user); """; try { String jsCode = integration.compileTypeScript(typeScriptCode); System.out.println("Compiled JavaScript:\n" + jsCode); Object result = integration.executeTypeScript(typeScriptCode); System.out.println("Execution result: " + result); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); } } } Best Practices and Production Considerations
Example 7: Production-Ready TypeScript Integration
import java.util.concurrent.*; import java.util.function.Function; public class ProductionTypeScriptEngine { private final ExecutorService executor; private final NodeTypeScriptExecutor tsExecutor; private final ConcurrentHashMap<String, Function<Object, Object>> cachedFunctions; public ProductionTypeScriptEngine() { this.executor = Executors.newFixedThreadPool(4); this.tsExecutor = new NodeTypeScriptExecutor(); this.cachedFunctions = new ConcurrentHashMap<>(); } public CompletableFuture<Object> executeTypeScriptFunction( String functionName, String typescriptCode, Object... args) { return CompletableFuture.supplyAsync(() -> { try { String argString = Arrays.stream(args) .map(arg -> { if (arg instanceof String) { return "\"" + arg + "\""; } return String.valueOf(arg); }) .reduce((a, b) -> a + ", " + b) .orElse(""); String executionCode = typescriptCode + "\nconst result = " + functionName + "(" + argString + ");\n" + "console.log(JSON.stringify(result));"; String output = tsExecutor.executeTypeScript(executionCode); return parseOutput(output); } catch (Exception e) { throw new CompletionException("TypeScript execution failed", e); } }, executor); } public <T, R> Function<T, R> compileTypeScriptFunction( String functionName, String typescriptCode, Class<R> returnType) { return cachedFunctions.computeIfAbsent(functionName + typescriptCode.hashCode(), key -> { return input -> { try { CompletableFuture<Object> future = executeTypeScriptFunction(functionName, typescriptCode, input); Object result = future.get(30, TimeUnit.SECONDS); return returnType.cast(result); } catch (Exception e) { throw new RuntimeException("TypeScript function execution failed", e); } }; })::apply; } private Object parseOutput(String output) { // Extract JSON from output String[] lines = output.split("\n"); for (int i = lines.length - 1; i >= 0; i--) { String line = lines[i].trim(); if (line.startsWith("{") || line.startsWith("[") || line.matches("-?\\d+(\\.\\d+)?")) { try { // Simple parsing - in production use proper JSON parser if (line.startsWith("{") || line.startsWith("[")) { return new org.json.JSONObject(line); // or JSONArray } else if (line.contains(".")) { return Double.parseDouble(line); } else { return Integer.parseInt(line); } } catch (Exception e) { return line; } } } return output; } public void shutdown() { executor.shutdown(); try { if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } } public static void main(String[] args) throws Exception { ProductionTypeScriptEngine engine = new ProductionTypeScriptEngine(); try { // Define TypeScript function String tsFunction = """ interface User { id: number; name: string; } function processUser(user: User): User { return { ...user, name: user.name.toUpperCase(), processed: true, timestamp: new Date().toISOString() }; } """; // Compile to Java function Function<Map<String, Object>, Map<String, Object>> processor = engine.compileTypeScriptFunction("processUser", tsFunction, Map.class); // Use like a regular Java function Map<String, Object> user = Map.of("id", 1, "name", "john doe"); Map<String, Object> result = processor.apply(user); System.out.println("Processed user: " + result); // Async execution CompletableFuture<Object> future = engine.executeTypeScriptFunction( "processUser", tsFunction, user); future.thenAccept(asyncResult -> { System.out.println("Async result: " + asyncResult); }).get(5, TimeUnit.SECONDS); } finally { engine.shutdown(); } } } Key Considerations
Performance
- GraalVM: Best performance for simple scripts
- Node.js Integration: Most practical for complex TypeScript
- Transpilation: Best for build-time optimization
Security
- Sandbox execution environments
- Validate and sanitize inputs
- Use process isolation for untrusted code
Error Handling
- Comprehensive exception handling
- TypeScript compilation error reporting
- Timeout management for long-running scripts
Deployment
- Bundle TypeScript compiler with application
- Consider Docker containers for Node.js dependencies
- Cache compiled scripts for performance
Conclusion
Embedding TypeScript in JVM applications provides powerful capabilities for dynamic scripting, configuration, and business logic. The best approach depends on your specific needs:
- GraalVM: For simple scripts with good performance
- Node.js Integration: For full TypeScript feature support
- Build-time Transpilation: For performance-critical applications
- Compiler API: For advanced integration scenarios
Each approach offers different trade-offs between performance, complexity, and feature completeness. Choose based on your application's requirements for type safety, execution speed, and deployment complexity.