Skip to main content

UI5 Tooling & TypeScript Integration: Modern Development Setup

SAP UI5 development has evolved dramatically. Gone are the days of manually managing libraries, using WebIDE, or fighting with XML views without type checking.

Modern UI5 development embraces TypeScript, offering type safety, better IDE support, and catching errors at compile-time instead of runtime.

This comprehensive guide will walk you through setting up a professional UI5 + TypeScript environmentfrom scratch, complete with modern tooling, build processes, and best practices.

Why TypeScript for UI5?

The Problem with Plain JavaScript UI5

// JavaScript - No type safety
var oModel = this.getView().getModel();
var sValue = oModel.getProperty("/customerName"); // What type is sValue?
oModel.setProperty("/totalAmount", "123"); // String or number? Error prone!

// Typos discovered only at runtime
this.getOwnerComponet().getRouter(); // Oops: "Component" typo
this._oDialg.open(); // Oops: "_oDialog" typo

The TypeScript Advantage

// TypeScript - Full type safety
const oModel = this.getView()?.getModel() as JSONModel;
const sValue: string = oModel.getProperty("/customerName") as string;
oModel.setProperty("/totalAmount", 123); // Number type enforced

// Typos caught at compile-time
this.getOwnerComponent().getRouter(); // ✓ Autocomplete suggests correct method
this._oDialog.open(); // ✓ IDE highlights typo immediately

Benefits of TypeScript in UI5

  • Type Safety – Catch errors before runtime
  • IntelliSense – Auto-completion for all UI5 APIs
  • Refactoring – Rename variables/methods safely across project
  • Documentation – Type definitions serve as inline documentation
  • Modern JS Features – async/await, destructuring, optional chaining
  • Better Tooling – ESLint, Prettier, VS Code integration

Prerequisites

Before starting, ensure you have:

  • Node.js – Version 18+ (LTS recommended)
  • npm or yarn – Package manager
  • VS Code – Recommended IDE
  • Git – Version control

Step 1: Install UI5 Tooling

# Install UI5 CLI globally
npm install -g @ui5/cli

# Verify installation
ui5 --version
# Output: 3.9.0 (example)

# Install SAP/open-oss UI5 TypeScript generator
npm install -g yo generator-easy-ui5

Step 2: Create New UI5 TypeScript Project

# Create project directory
mkdir my-ui5-ts-app
cd my-ui5-ts-app

# Initialize with easy-ui5 generator
yo easy-ui5 project

# Interactive prompts:
# ? Project name: my-ui5-ts-app
# ? Namespace: com.mycompany
# ? Framework: OpenUI5
# ? Framework version: 1.120.0
# ? Enable TypeScript: Yes
# ? Add ESLint: Yes
# ? Add Karma for testing: Yes
# ? Author: Your Name
# ? License: Apache-2.0

# Install dependencies
npm install

Alternative: Manual Setup

# Initialize package.json
npm init -y

# Install UI5 dependencies
npm install --save-dev @ui5/cli @openui5/ts-types-esm

# Install TypeScript
npm install --save-dev typescript @types/node

# Install build tools
npm install --save-dev rimraf npm-run-all

# Create tsconfig.json
cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "lib": ["ES2022", "DOM"],
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "types": ["@openui5/ts-types-esm"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
EOF

Step 3: Project Structure

my-ui5-ts-app/
├── src/
│   ├── controller/
│   │   ├── BaseController.ts
│   │   └── Main.controller.ts
│   ├── model/
│   │   ├── formatter.ts
│   │   └── models.ts
│   ├── view/
│   │   ├── Main.view.xml
│   │   └── App.view.xml
│   ├── i18n/
│   │   └── i18n.properties
│   ├── css/
│   │   └── style.css
│   ├── Component.ts
│   ├── manifest.json
│   └── index.html
├── test/
│   ├── unit/
│   │   └── controller/
│   └── integration/
├── dist/              # Build output
├── package.json
├── tsconfig.json
├── ui5.yaml
└── README.md

Step 4: Configure ui5.yaml

specVersion: "3.0"
metadata:
  name: com.mycompany.myui5tsapp
type: application
framework:
  name: OpenUI5
  version: "1.120.0"
  libraries:
    - name: sap.m
    - name: sap.ui.core
    - name: sap.ui.layout
builder:
  customTasks:
    - name: ui5-tooling-transpile-task
      afterTask: replaceVersion
      configuration:
        transformModulesToUI5: true
server:
  customMiddleware:
    - name: ui5-middleware-livereload
      afterMiddleware: compression
    - name: ui5-tooling-transpile-middleware
      afterMiddleware: compression

Step 5: Create Component.ts

import UIComponent from "sap/ui/core/UIComponent";
import { support } from "sap/ui/Device";
import JSONModel from "sap/ui/model/json/JSONModel";
import models from "./model/models";

/**
 * @namespace com.mycompany.myui5tsapp
 */
export default class Component extends UIComponent {
  public static metadata = {
    manifest: "json"
  };

  /**
   * The component is initialized by UI5 automatically during the startup
   */
  public init(): void {
    // Call the base component's init function
    super.init();

    // Set the device model
    this.setModel(models.createDeviceModel(), "device");

    // Create the views based on the url/hash
    this.getRouter().initialize();
  }

  /**
   * This method can be called to determine whether the sapUiSizeCompact
   * design mode class should be set
   */
  public getContentDensityClass(): string {
    if (!support.touch) {
      return "sapUiSizeCompact";
    }
    return "sapUiSizeCozy";
  }
}

Step 6: Create BaseController.ts

import Controller from "sap/ui/core/mvc/Controller";
import UIComponent from "sap/ui/core/UIComponent";
import Router from "sap/ui/core/routing/Router";
import History from "sap/ui/core/routing/History";
import Component from "../Component";
import Model from "sap/ui/model/Model";
import ResourceModel from "sap/ui/model/resource/ResourceModel";
import ResourceBundle from "sap/base/i18n/ResourceBundle";

/**
 * @namespace com.mycompany.myui5tsapp.controller
 */
export default abstract class BaseController extends Controller {
  /**
   * Convenience method for accessing the router
   */
  public getRouter(): Router {
    return (this.getOwnerComponent() as UIComponent).getRouter();
  }

  /**
   * Convenience method for getting the view model by name
   */
  public getModel(sName?: string): Model {
    return this.getView()?.getModel(sName) as Model;
  }

  /**
   * Convenience method for setting the view model
   */
  public setModel(oModel: Model, sName?: string): void {
    this.getView()?.setModel(oModel, sName);
  }

  /**
   * Getter for the resource bundle
   */
  public getResourceBundle(): ResourceBundle {
    const oModel = this.getModel("i18n") as ResourceModel;
    return oModel.getResourceBundle() as ResourceBundle;
  }

  /**
   * Navigate back in browser history
   */
  public navBack(): void {
    const oHistory = History.getInstance();
    const sPreviousHash = oHistory.getPreviousHash();

    if (sPreviousHash !== undefined) {
      window.history.go(-1);
    } else {
      this.getRouter().navTo("main", {}, true);
    }
  }
}

Step 7: Create Main.controller.ts

import MessageBox from "sap/m/MessageBox";
import BaseController from "./BaseController";
import JSONModel from "sap/ui/model/json/JSONModel";
import formatter from "../model/formatter";

interface ICustomer {
  id: string;
  name: string;
  email: string;
  totalOrders: number;
}

/**
 * @namespace com.mycompany.myui5tsapp.controller
 */
export default class Main extends BaseController {
  public formatter = formatter;

  public onInit(): void {
    // Initialize view model
    const oViewModel = new JSONModel({
      busy: false,
      customers: [] as ICustomer[],
      selectedCustomer: null as ICustomer | null
    });
    this.setModel(oViewModel, "view");

    // Load initial data
    this.loadCustomers();
  }

  private async loadCustomers(): Promise<void> {
    const oViewModel = this.getModel("view") as JSONModel;
    oViewModel.setProperty("/busy", true);

    try {
      // Simulate API call
      await this.delay(1000);
      
      const aCustomers: ICustomer[] = [
        { id: "1", name: "John Doe", email: "john@example.com", totalOrders: 15 },
        { id: "2", name: "Jane Smith", email: "jane@example.com", totalOrders: 23 },
        { id: "3", name: "Bob Johnson", email: "bob@example.com", totalOrders: 8 }
      ];

      oViewModel.setProperty("/customers", aCustomers);
    } catch (error) {
      MessageBox.error("Failed to load customers: " + (error as Error).message);
    } finally {
      oViewModel.setProperty("/busy", false);
    }
  }

  public onCustomerPress(oEvent: any): void {
    const oSource = oEvent.getSource();
    const oContext = oSource.getBindingContext("view");
    const oCustomer = oContext.getObject() as ICustomer;

    MessageBox.information(
      `Customer: ${oCustomer.name}\nEmail: ${oCustomer.email}\nTotal Orders: ${oCustomer.totalOrders}`
    );
  }

  public async onRefreshPress(): Promise<void> {
    await this.loadCustomers();
    MessageBox.success("Customers refreshed successfully");
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Step 8: Create formatter.ts

/**
 * @namespace com.mycompany.myui5tsapp.model
 */
export default {
  /**
   * Rounds the number value to 2 decimal places
   */
  formatCurrency(value: number | string): string {
    if (!value) {
      return "0.00";
    }
    const numValue = typeof value === "string" ? parseFloat(value) : value;
    return numValue.toFixed(2);
  },

  /**
   * Formats status to display text
   */
  formatStatus(status: string): string {
    const statusMap: Record<string, string> = {
      "A": "Active",
      "I": "Inactive",
      "P": "Pending",
      "C": "Completed"
    };
    return statusMap[status] || "Unknown";
  },

  /**
   * Returns icon based on status
   */
  getStatusIcon(status: string): string {
    const iconMap: Record<string, string> = {
      "A": "sap-icon://accept",
      "I": "sap-icon://decline",
      "P": "sap-icon://pending",
      "C": "sap-icon://complete"
    };
    return iconMap[status] || "sap-icon://question-mark";
  },

  /**
   * Returns state based on value
   */
  getValueState(value: number): string {
    if (value >= 20) return "Success";
    if (value >= 10) return "Warning";
    return "Error";
  }
};

Step 9: Update package.json Scripts

{
  "name": "my-ui5-ts-app",
  "version": "1.0.0",
  "scripts": {
    "start": "ui5 serve --open",
    "build": "npm run clean && npm run build:ts && ui5 build --clean-dest",
    "build:ts": "tsc",
    "clean": "rimraf dist",
    "lint": "eslint src --ext .ts",
    "lint:fix": "eslint src --ext .ts --fix",
    "test": "karma start",
    "watch": "npm-run-all --parallel watch:*",
    "watch:ts": "tsc --watch",
    "watch:ui5": "ui5 serve"
  },
  "dependencies": {},
  "devDependencies": {
    "@openui5/ts-types-esm": "^1.120.0",
    "@types/node": "^20.0.0",
    "@ui5/cli": "^3.9.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.0.0",
    "npm-run-all": "^4.1.5",
    "rimraf": "^5.0.0",
    "typescript": "^5.3.0",
    "ui5-middleware-livereload": "^3.0.0",
    "ui5-tooling-transpile": "^3.0.0"
  }
}

Step 10: ESLint Configuration

// .eslintrc.json
{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "plugins": ["@typescript-eslint"],
  "parserOptions": {
    "ecmaVersion": 2022,
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "warn",
    "@typescript-eslint/no-explicit-any": "warn",
    "@typescript-eslint/no-unused-vars": "error",
    "no-console": "warn"
  },
  "env": {
    "browser": true,
    "node": true,
    "es2022": true
  }
}

Step 11: VS Code Configuration

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.preferences.importModuleSpecifier": "relative",
  "files.exclude": {
    "**/.git": true,
    "**/node_modules": true,
    "**/dist": true
  }
}

// .vscode/extensions.json
{
  "recommendations": [
    "esbenp.prettier-vscode",
    "dbaeumer.vscode-eslint",
    "SAPOSS.vscode-ui5-language-assistant"
  ]
}

Advanced TypeScript Features in UI5

Type-Safe Event Handlers

import Event from "sap/ui/base/Event";
import Button from "sap/m/Button";

public onPress(oEvent: Event): void {
  const oButton = oEvent.getSource() as Button;
  const sText = oButton.getText();
  console.log("Button text:", sText);
}

Generic Model Access

private getViewModel<T>(): JSONModel {
  return this.getModel("view") as JSONModel;
}

private getViewProperty<T>(sPath: string): T {
  return this.getViewModel().getProperty(sPath) as T;
}

// Usage
const customers = this.getViewProperty<ICustomer[]>("/customers");

Async/Await with OData

private async loadDataFromOData(): Promise<void> {
  const oModel = this.getModel() as ODataModel;
  
  try {
    const data = await new Promise((resolve, reject) => {
      oModel.read("/Customers", {
        success: resolve,
        error: reject
      });
    });
    
    this.getViewModel().setProperty("/customers", data);
  } catch (error) {
    MessageBox.error("Failed to load data");
  }
}

Testing with TypeScript

// test/unit/controller/Main.controller.test.ts
import Main from "../../../src/controller/Main.controller";

QUnit.module("Main Controller");

QUnit.test("Should initialize view model", (assert: Assert) => {
  const oController = new Main("testController");
  oController.onInit();
  
  const oViewModel = oController.getModel("view");
  assert.ok(oViewModel, "View model exists");
  assert.strictEqual(
    oViewModel.getProperty("/busy"),
    false,
    "Busy flag is false"
  );
});

Building and Deploying

# Development
npm start

# Production build
npm run build

# Output in dist/ folder
dist/
├── Component-preload.js
├── manifest.json
├── index.html
└── resources/

Best Practices

✅ DO

  • Use strict TypeScript settings
  • Define interfaces for data structures
  • Leverage type inference when possible
  • Use async/await for asynchronous operations
  • Create reusable base controllers
  • Organize code into logical modules
  • Use ESLint and Prettier
  • Write unit tests

❌ AVOID

  • Using any type excessively
  • Ignoring TypeScript errors
  • Mixing JavaScript and TypeScript
  • Not defining return types
  • Skipping interface definitions

Conclusion

Setting up UI5 with TypeScript transforms development from error-prone JavaScript totype-safe, maintainable, and productive code.

Key benefits achieved:

  • Compile-time error detection
  • Superior IDE support
  • Better code documentation
  • Safer refactoring
  • Modern JavaScript features

TypeScript + UI5 = Professional, scalable enterprise applications

About the Author: Yogesh Pandey is a passionate developer and consultant specializing in SAP technologies and full-stack development.