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" typoThe 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 immediatelyBenefits 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"]
}
EOFStep 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: compressionStep 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
anytype 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
