Hilla Documentation

TypeScript Endpoints Generator

Understanding the Hilla TypeScript endpoints generator, type nullability, and how the generator generates TypeScript from the OpenAPI specification.

The TypeScript generator produces TypeScript files based on the information from an OpenAPI document that’s generated from Java files in the src/main/java folder by default.

Note

Hilla uses the OpenAPI Specification as a middle layer between Java endpoints and TypeScript endpoint clients. The current implementation is based on OpenAPI specification 3.0. For details, refer to the appendix at the end of this page.

Examples

A simple generated TypeScript file looks like the following snippet:

/**
 * User endpoints.
 *
 * This module has been generated from UserEndpoints.java
 * @module UserEndpoints
 */
import client from './connect-client.default';

/**
 * Check if a user is admin or not.
 *
 * @param id User id to be checked
 * Return Return true if the given user is an admin, otherwise false.
 */
export async function isAdmin(
  id?: number
) {
  return await client.call('UserEndpoints', 'isAdmin', {id});
}

The line import client from './connect-client.default' is a static part of any generated file. connect-client.default.ts is another generated file, which includes default configurations for the ConnectClient and exports its instance as client.

Each method in the generated modules corresponds to a Java method in @Endpoint-annotated classes. For example, the following Java code corresponds to the generated UserEndpoints.ts:

/**
 * User endpoints.
 */
@Endpoint
public class UserEndpoints {
    /**
     * Check if a user is admin or not.
     *
     * @param id
     *            User id to be checked
     * @return Return true if the given user is an admin, otherwise false.
     */
    public boolean isAdmin(long id) {
        return id == 0;
    }
}
Note
For more information about type mapping between Java and TypeScript, refer to type conversion.

Type Nullability

Types are either nullable (optional) or non-nullable (required). By default, types are mapped and generated using the Java rules:

  • Any primitive type, such as int, is non-nullable.

  • Any reference type, such as String or Integer, is nullable.

  • A collection accepts null, unless the collection item type is primitive.

  • A map accepts null, unless the collection item type is primitive.

Any of these nullable types can be made non-nullable by applying a @Nonnull annotation. You can use any annotation that has the name nonnull (case-insensitive). For example:

  • javax.annotation.Nonnull

  • edu.umd.cs.findbugs.annotations.NonNull

  • lombok.NonNull

  • android.support.annotation.NonNull

  • org.eclipse.jdt.annotation.NonNull

  • any other annotation (including custom) that has the name nonnull (case-insensitive).

Endpoint Functions

For an endpoint function, nullable elements are:

  • Function parameter type. Arguments cannot be omitted, even when the parameter types are nullable. To receive a null parameter value in Java, send an undefined argument in the endpoint function call.

  • Function return type.

@Endpoint
class PersonEndpoint {
    // Person must have at least the first and the last name
    public void setFullName(@Nonnull String firstName, @Nonnull String lastName, String middleName) {
        // omitted code
    }

    // Full name must exist
    @Nonnull
    public String getFullName() {
        // omitted code
    }

    // Person can have no connections with other people. But if they have,
    // the connection cannot be null.
    public Map<String, @Nonnull String> getConnections() {
        // omitted code
    }
}
export async function setName(
  firstName: string,
  lastName: string,
  middleName: string | undefined
) {
  return client.call('PersonEndpoint', 'setFullName', {firstName, lastName, middleName});
}

export async function getFullName(): Promise<string> {
  return client.call('PersonEndpoint', 'getFullName');
}

export async function getConnections(): Promise<Record<string, string> | undefined> {
  return client.call('PersonEndpoint', 'getConnections');
}

Data Class Properties

Properties of data classes are nullable. Unlike the function parameters, all nullable properties can be omitted.

public class MyBean {
    private long id;
    @Nonnull
    private String value;
    private String description;
    private Map<String, String> map;
    @Nonnull
    private List<String> list;
}
export default interface MyBean {
  id: number;
  value: string;
  description?: string;
  map?: Record<string, string | undefined>;
  list: Array<string | undefined>;
}

Collection Item Types

The collection item type is nullable.

public class MyBean {
    private List<String> list;
    private List<@Nonnull String> nonNullableList;
    private Map<String, String> map;
    private Map<String, @Nonnull String> nonNullableMap;
}
export default interface MyBean {
  list?: Array<string | undefined>;
  nonNullableList?: Array<string>;
  map?: Record<string, string | undefined>;
  nonNullableMap?: Record<string, string>;
}

Enum

The Java enum type is mapped to an enum TypeScript type. It’s an object type, so you can work with it as you work with regular TypeScript objects.

public enum Enumeration {
    FIRST,
    SECOND,
}
export enum Enumeration {
  FIRST = "FIRST",
  SECOND = "SECOND"
}
Note
Complex Java enums mapping

The enum type is mapped in a simple way. No constructor-related Java features are available in the TypeScript enum.

public enum Enumeration {
    FIRST("ONE"),
    SECOND("TWO");

    private String value;

    public Enumeration(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}
export enum Enumeration {
  FIRST = "FIRST",
  SECOND = "SECOND"
}

Appendix: How the Generator Generates TypeScript From OpenAPI Specification

Modules

The generator collects all the tags fields of all operations in the OpenAPI document. Each tag generates a corresponding TypeScript file. The tag name is used for TypeScript module name, as well as the file name. The TsDoc of the class is fetched from the description field of the tag object that has the same name as the class.

Methods

Each exported method in a module corresponds to a POST operation of a path item in paths object.

Note

Currently, the generator only supports the POST operation. If a path item contains operations other than POST, the generator will stop processing.

The path must start with /, as described in Patterned Fields. It’s parsed as /<endpoint name>/<method name>, which is used as a parameter to call to Java endpoints in the backend. The method name from the path is also reused as the method name in the generated TypeScript file.

Method Parameters

The parameters of the method are taken from the application/json content of the request body object. To get the result as [UserEndpoint.ts], the request body content should be:

{
 "content": {
    "application/json": {
      "schema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "number",
            "description": "User id to be checked"
          }
        }
      }
    }
  }
}

The type and description of each property are used for the TsDoc that describes the parameter in more detail.

Note

All the other content types of the request body object are ignored by the Hilla generator. This means that a method that doesn’t have the application/json content type is considered to be one with no parameters.

Method Return Type

The return type and its description are taken from the 200 response object. As with the request body object, the generator is only interested in the application/json content type. The schema type indicates the return type and the description describes the result. Here is an example of a response object:

{
  "200": {
    "description": "Return true if the given user is an admin, otherwise false.",
    "content": {
      "application/json": {
        "schema": {
          "type": "boolean"
        }
      }
    }
  }
}
Note

Currently, the generator only recognizes 200 response objects. Other response objects are ignored.

Method TsDoc

The TsDoc of the generated method is stored as the description value of the POST operation in the path item. A valid POST operation combined with [request-body] and [response-object] would look like this:

{
  "tags": ["UserEndpoint"], // (1)
  "description": "Check if a user is admin or not.",
  "requestBody": {
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "id": {
              "type": "number",
              "description": "User id to be checked"
            }
          }
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Return true if the given user is an admin, otherwise false.",
      "content": {
        "application/json": {
          "schema": {
            "type": "boolean"
          }
        }
      }
    }
  }
}
  1. As mentioned in the operation object specification, in the Hilla generator, tags are used to classify operations into TypeScript files. This means that each tag will have a corresponding generated TypeScript file. Operations that contain more than one tag will appear in all the generated files. Operations with empty tags will be placed in the Default.ts file.

Note

Although multiple tags don’t break the generator, it might be confusing at development time if there are two identical methods in different TypeScript files. It’s recommended to have only one tag per operation.

Here is an example OpenAPI document that could generate previous UserEndpoint.ts.

{
  "openapi" : "3.0.1",
  "info" : {
    "title" : "My example application",
    "version" : "1.0.0"
  },
  "servers" : [ {
    "url" : "https://myhost.com/myendpoint",
    "description" : "Hilla backend server"
  } ],
  "tags" : [ {
    "name" : "UserEndpoint",
    "description" : "User endpoint class."
  } ],
  "paths" : {
    "/UserEndpoint/isAdmin" : {
      "post": {
        "tags": ["UserEndpoint"],
        "description": "Check if a user is admin or not.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [ "id" ],
                "properties": {
                  "id": {
                    "type": "number",
                    "description": "User id to be checked"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return true if the given user is an admin, otherwise false.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      }
    }
  }
}