Why TypeScript for UI5
SAPUI5 has shipped official TypeScript definitions for years, and the framework itself is developed in TypeScript. You get real autocompletion, compile-time checks, and refactoring that actually works across controllers and views.
TypeScript doesn't change what UI5 can do — it changes how fast you can do it safely. The typo that ships to production in JavaScript becomes a red squiggle in your editor.
Setting Up a Project
The fastest path is the UI5 Tooling TypeScript setup. Install the middleware and task, then add a tsconfig.json. UI5 Tooling transpiles your .ts sources during build.
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"types": ["@openui5/ts-types"]
},
"include": ["webapp/**/*"]
}
The @openui5/ts-types package provides the framework definitions. Keep it version-aligned with your UI5 runtime to avoid mismatched APIs.
Your First Typed Controller
Controllers extend the typed base class and declare their view type. Event handlers get proper parameter types instead of any.
import Controller from "sap/ui/core/mvc/Controller";
import Button from "sap/m/Button";
export default class MainController extends Controller {
onInit(): void {
// this.byId is now typed
}
onPress(oEvent: Button["$Press"]): void {
const oButton = oEvent.getSource();
oButton.setText("Clicked");
}
}
Notice the event type syntax: Button["$Press"] references the generated event interface. It looks odd at first, then it clicks.
Typing Views and Fragments
XML views work unchanged — TypeScript only types the controller side. For fragments loaded via loadFragment, cast the result to the expected control type.
const oDialog = await this.loadFragment({
name: "my.app.view.Dialog"
}) as Dialog;
oDialog.open();
Keep XML views as XML. TypeScript's job is the controller logic — don't try to type your way around the view layer.
Common Gotchas
this.byIdreturnsControl | undefined: narrow it with a type guard or a non-null assertion after a check.- Models are untyped by default: define interfaces for your OData payloads and cast
getData()results. - Strict mode fights legacy patterns: turn strict on from day one; retrofitting it onto a converted JS codebase is painful.
- Generated event types: the
$EventNamelookup pattern works for most controls, but always check the API reference — a few events use custom parameter interfaces.
Typing Your Models
Define interfaces for the data your app actually consumes. One small file of interfaces replaces dozens of defensive any casts scattered through your controllers.
interface Order {
OrderId: string;
CustomerName: string;
NetValue: number;
Items: OrderItem[];
}
const oData = oModel.getData() as { Orders: Order[] };
From here, autocompletion works on every property access, and a renamed backend field surfaces as a compile error instead of a runtime blank screen.
Wrapping Up
Say you have a medium-sized app with a dozen controllers — migrating file by file, starting with new features, gets you typed safety without a big-bang rewrite. The editor support alone pays for the setup cost.
Already using TypeScript with UI5? Share your setup tips in the comments.