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.