Skip to main content

JavaScript LST examples

When building recipes for JavaScript and TypeScript code, it's important to understand how OpenRewrite Lossless Semantic Trees (LSTs) correspond to code. You couldn't, for example, properly transform an arrow function without knowing that JS.ArrowFunction is the class used to represent arrow functions.

This doc will walk you through everything you need to know to begin working with the JavaScript LST.

The hybrid LST approach

JavaScript and TypeScript code in OpenRewrite is represented using a hybrid approach:

  • JavaScript-specific LST types (JS.*) - These handle JavaScript and TypeScript-specific features like arrow functions, template literals, destructuring, type annotations, and ES6 modules.
  • Shared Java LST types (J.*) - OpenRewrite reuses Java LST nodes for constructs that are semantically similar between the two languages, such as classes, method declarations, variable declarations, and literals.

This design choice avoids code duplication and enables recipes that work on shared constructs to potentially work across both languages. While this may seem unusual at first, understanding this hybrid approach is important when working with JavaScript LSTs as you'll encounter both types in practice.

tip

To see how these LST types work together in real code, check out the putting it all together section at the end of this document.

JavaScript-specific LST types

This section covers some of the most important JS types that are unique to JavaScript and TypeScript. These types handle language features that don't exist in Java.

note

This is not an exhaustive list. Instead, this section contains the most common LST elements you'll use when writing recipes.

CompilationUnit

A JS.CompilationUnit is the root of the JavaScript/TypeScript LST. All other elements must be contained inside of this to represent valid JavaScript or TypeScript code.

Code example:

hello.js
// This entire file is a CompilationUnit
import { greeting } from './messages';

function sayHello(name) {
console.log(greeting + name);
}

export default sayHello;

ArrowFunction

A JS.ArrowFunction represents arrow function expressions (() => {}). It contains a Lambda for parameters and body, and optional TypeInfo for the return type.

Code example:

const greet = ({ name }: User): string => {
return `Hello, ${name}!`;
};

Await

A JS.Await represents an await expression in async functions. It contains the expression being awaited.

Code example:

await fetch(url)

Binary

A JS.Binary represents binary expressions with JavaScript-specific operators that don't exist in Java, such as strict equality/inequality operators.

Code example:

value === null       // strict equality
obj !== undefined // strict inequality
note

Common binary operators like +, -, * are represented using J.Binary (the shared Java type) – not JS.Binary.

Delete

A JS.Delete represents the delete operator.

Code example:

delete obj.property

FunctionCall

A JS.FunctionCall represents function/method calls in JavaScript that cannot be modeled as J.MethodInvocation. This is used for call syntax that's not possible in Java.

Code example:

function greet(name: string) {
return function (greeting: string) {
return `${greeting}, ${name}!`;
};
}

// The interesting part: greet("Alice")("Hello")
console.log(greet("Alice")("Hello"));

In the expression greet("Alice")("Hello"):

  • greet("Alice") is modeled as a J.MethodInvocation
  • ("Hello") is modeled as a JS.FunctionCall (this type of functionality is not possible in Java)

Export

A JS.ExportDeclaration represents named exports, while JS.ExportAssignment represents default exports.

Code examples:

export { Store, greet };   // ExportDeclaration
export default User; // ExportAssignment

Import

A JS.Import represents an ES6 import statement. It contains an ImportClause (what is being imported) and the module path as a Literal.

Code examples:

import { Helper } from './utils';
import type { Config } from './types';
note

CommonJS require() calls are not represented as JS.Import. Instead, they're modeled as J.MethodInvocation, which illustrates the hybrid nature of JavaScript LST modeling.

For example, const fs = require('fs') would be represented as a J.VariableDeclarations – with a J.MethodInvocation as its initializer.

Intersection

A JS.Intersection represents types combined with the & operator in TypeScript.

Code examples:

User & Timestamped
{ name: string } & { age: number }
Readable & Writable
BaseClass & { id: string }

ObjectBindingPattern

A JS.ObjectBindingPattern represents destructuring patterns in function parameters or variable declarations.

Code example:

({ name }: User)

TemplateExpression

A JS.TemplateExpression represents template literals (backtick strings) that can contain embedded expressions using ${}.

Code example:

`Hello, ${name}!`

Union

A JS.Union represents types combined with the | operator in TypeScript.

Code examples:

string | number
'active' | 'inactive' | 'pending'
User | null
{ status: 'ok' } | { error: string }

Shared types with Java

These LST types are shared between JavaScript and Java, as the constructs they represent are semantically similar in both languages. When you see these in a JavaScript/TypeScript LST, they're using the same classes that Java uses, which enables cross-language recipe patterns.

info

Understanding these shared types is important because:

  • You'll see J.* prefixes in JavaScript LST trees
  • Recipes written for Java constructs might also work on JavaScript code
  • The API and structure are consistent across both languages

ClassDeclaration

A J.ClassDeclaration is used for JavaScript/TypeScript classes, interfaces, and enums. This is a shared type with Java since the class syntax is similar.

Code examples:

interface User {
id: ID;
name: string;
active?: boolean;
}

class Store<T> {
private items: T[] = [];
// ...
}

FieldAccess

A J.FieldAccess represents property access using dot notation.

Code examples:

this.items
response.json
obj.property

Identifier

A J.Identifier represents any name in the code.

Code examples:

Helper, Config, ID, User, Store, greet, name, items

Literal

A J.Literal represents literal values.

Code examples:

'./utils'
'./types'
1
2
'value'

MethodDeclaration

Functions in JavaScript/TypeScript are represented using J.MethodDeclaration for function declarations and class methods. This includes async functions, generic functions, and methods with TypeScript type annotations.

Code examples:

// Function declaration
function greet(name: string): string {
return `Hello, ${name}!`;
}

// Class method
add(item: T): void {
this.items.push(item);
}

// Async function
async fetch(url: string): Promise<T[]> {
const response = await fetch(url);
return response.json();
}

MethodInvocation

A J.MethodInvocation represents function and method calls. The structure is similar to Java, with a select expression (for method calls) and arguments.

Code examples:

this.items.push(item)
fetch(url)
response.json()

VariableDeclarations

A J.VariableDeclarations represents variable declarations with const, let, or var. In TypeScript, these can include type annotations via JS.TypeInfo.

Code examples:

const greet = ({ name }: User): string => { ... };
const sum = 1 + 2;
private items: T[] = [];

Putting it all together

One of the key aspects of LSTs is that they're compositional - complex code structures are built from simpler LST components nested within each other. A single line of code often contains many different LST types working together.

Let's look at some examples to see how this works.

Example 1: Arrow function with destructuring

const greet = ({ name, age }: Person): string => {
return `Hello ${name}, you are ${age} years old`;
};

This code breaks down into:

  • J.VariableDeclarations: The entire const greet = ... declaration
    • J.VariableDeclarations.NamedVariable: Contains the variable name and value
      • J.Identifier: The variable name greet
      • JS.ArrowFunction: The arrow function
        • J.Lambda: Contains parameters and body
          • J.Lambda.Parameters: The function parameters
            • J.VariableDeclarations: The parameter declaration
              • JS.TypeInfo: The : Person type annotation
                • J.Identifier: Person
              • JS.ObjectBindingPattern: The { name, age } destructuring
                • JS.BindingElement: name
                • JS.BindingElement: age
          • J.Block: The function body
            • J.Return: The return statement
              • JS.TemplateExpression: The template literal
                • J.Literal: "Hello ${"
                • JS.TemplateExpression.Span: First interpolation
                  • J.Identifier: name
                  • J.Literal: "}, you are ${"
                • JS.TemplateExpression.Span: Second interpolation
                  • J.Identifier: age
                  • J.Literal: "} years old"
        • JS.TypeInfo: The : string return type
          • J.Identifier: string

Example 2: Async function with await

async function fetchData(url: string): Promise<string> {
const response = await fetch(url);
return response.text();
}

This function contains:

  • J.MethodDeclaration: The entire function declaration
    • J.Modifier: async modifier
    • JS.TypeInfo: The : Promise<string> return type
      • J.ParameterizedType: Promise<string>
        • J.Identifier: Promise
        • J.Identifier: string
    • J.Identifier: The function name fetchData
    • J.VariableDeclarations: The url parameter
      • JS.TypeInfo: The : string type annotation
        • J.Identifier: string
      • J.VariableDeclarations.NamedVariable: url
    • J.Block: The function body
      • J.VariableDeclarations: const response = await fetch(url)
        • J.VariableDeclarations.NamedVariable
          • J.Identifier: response
          • JS.Await: The await expression
            • J.MethodInvocation: fetch(url)
              • J.Identifier: fetch
              • J.Identifier: url
      • J.Return: return response.text()
        • J.MethodInvocation: response.text()
          • J.Identifier: response
          • J.Identifier: text

Example 3: Function chaining with JS.FunctionCall

function createValidator(type: string) {
return function(value: any) {
if (type === 'email') {
return value !== null && value.includes('@');
}
return value !== undefined;
};
}

const isValid = createValidator('email')('test@example.com');

This example demonstrates both JS.Binary operators and JS.FunctionCall:

  • J.MethodDeclaration: createValidator function
    • J.Identifier: createValidator
    • J.VariableDeclarations: The type parameter
      • JS.TypeInfo: The : string type annotation
        • J.Identifier: string
      • J.VariableDeclarations.NamedVariable: type
    • J.Block: Function body
      • J.Return: Returns the inner function
        • JS.StatementExpression: Wraps the function expression
          • J.MethodDeclaration: The anonymous inner function
            • J.VariableDeclarations: The value parameter with : any
            • J.Block: Inner function body
              • J.If: The email check
                • J.ControlParentheses: Wraps condition
                  • JS.Binary: type === 'email' (JS-specific operator)
                    • J.Identifier: type
                    • J.Literal: 'email'
                • J.Block: If body
                  • J.Return: Compound boolean expression
                    • J.Binary: && (standard operator)
                      • JS.Binary: value !== null (JS-specific)
                        • J.Identifier: value
                        • J.Literal: null
                      • J.MethodInvocation: value.includes('@')
              • J.Return: Default return
                • JS.Binary: value !== undefined (JS-specific)
                  • J.Identifier: value
                  • J.Identifier: undefined
  • J.VariableDeclarations: The isValid constant
    • J.VariableDeclarations.NamedVariable: isValid
      • JS.FunctionCall: The chained function call
        • J.MethodInvocation: createValidator('email')
          • J.Identifier: createValidator
          • J.Literal: 'email'
        • J.Literal: 'test@example.com' (the argument to the returned function)

Example 4: Class with generics

class DataStore<T> {
private items: T[] = [];

add(item: T): void {
this.items.push(item);
}
}

This class contains:

  • J.ClassDeclaration: The entire class
    • J.Identifier: DataStore
    • J.TypeParameter: The generic parameter T
      • J.Identifier: T
    • J.Block: The class body
      • J.VariableDeclarations: private items: T[] = []
        • J.Modifier: private
        • JS.TypeInfo: The : T[] type annotation
          • J.ArrayType: T[]
            • J.Identifier: T
        • J.VariableDeclarations.NamedVariable: items = []
          • J.Identifier: items
          • J.NewArray: The empty array initializer
      • J.MethodDeclaration: The add method
        • JS.TypeInfo: The : void return type
          • J.Identifier: void
        • J.Identifier: add
        • J.VariableDeclarations: The item parameter
          • JS.TypeInfo: The : T type annotation
            • J.Identifier: T
          • J.VariableDeclarations.NamedVariable: item
        • J.Block: The method body
          • J.MethodInvocation: this.items.push(item)
            • J.FieldAccess: this.items
              • J.Identifier: this
              • J.Identifier: items
            • J.Identifier: push
            • J.Identifier: item

Key insights

  1. Hierarchical structure: LSTs form a tree structure where complex expressions contain simpler ones
  2. Mixed types: JavaScript code often mixes JS.* types (JavaScript-specific) with J.* types (shared with Java)
  3. Recursive composition: The same LST type can appear at multiple levels (e.g., J.Identifier appears throughout)
  4. Semantic preservation: Each LST preserves not just the code's meaning but also its exact formatting and structure

Understanding this compositional nature is crucial when writing recipes, as you'll often need to:

  • Navigate through multiple layers of LSTs to find the element you want to modify
  • Understand the parent-child relationships between different LST types
  • Preserve the structure while making targeted changes