Embedding TypeScript in JVM: Complete Guide for Java Developers

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

  1. GraalVM Polyglot - Direct TypeScript execution
  2. Nashorn with TypeScript Compilation - Legacy approach
  3. Node.js Process Integration - Most practical approach
  4. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper