- Brief TypeScript Introduction
- The Type Definition Files provided for UI5
- How does TypeScript UI5 Code look? - The Sample App in TypeScript
- Writing UI5 Apps in TypeScript
- Converting UI5 Apps from JavaScript to TypeScript
- Resources
TypeScript is:
- JavaScript plus types
- Purely used at development time (NOT understood by browsers at runtime, so there must be a build step before running it)
- Developed by Microsoft, but open-source and widely adopted
The basic assignment of a type (here: "number") to a variable looks like this:
var someNumber: number;
But of course there are more language constructs, used for defining structured types, classes, enums and so on. For a real introduction to TypeScript with tutorials and an online playground, head over to the language homepage at www.typescriptlang.org. They also offer a handy 5-minutes introduction for JavaScript developers.
When developers write TypeScript code, the type information is scattered across the code. But one can also create separate type definition files for already existing JavaScript libraries. Such files have the extension *.d.ts
and these are exactly what we are providing for UI5 now. For many other JavaScript libraries such type definition files can be found at definitelytyped.org.
The SAPUI5 type definitions are provided via npm under the name "@sapui5/types". The OpenUI5 subset is available as "@openui5/types
" and also made available by DefinitelyTyped as "@types/openui5
" (for the differences, which are related to the versioning, see this explanation). There is one *.d.ts file per UI5 library.
IMPORTANT: As we want to enable and promote using modern JavaScript, these *.d.ts files are written in a way that supports loading UI5 module with ES module syntax (instead of using the UI5 API sap.ui.require(...)
or sap.ui.define(...)
) and defining classes with ES class syntax (instead of using the UI5 API SomeClass.extend(...)
)). If you use our *.d.ts
files, this is how you should write your UI5 apps.
- The type definitions are work in progress with NO COMPATIBILITY GUARANTEES! Changes will happen.
- The string syntax for bindings is not supported yet. This means instead of e.g.
new Button({text: "{myModel>someText}"})
one has to use the object syntax to prevent TypeScript from complaining:new Button({text: {path: "myModel>someText"}})
- The types of UI5 API parameter and return values are not always precisely defined. This happens especially for nested structures and for the types with which Promises resolve. This is because the type definition files are generated from the regular JSDoc documentation, which not always has complete
@param
definitions, but occasionally explains some parts in plaintext instead. When you notice a place where types are not fully defined, please open a GitHub issue. Fixing it will not only improve the TypeScript experience, but also make our regular API documentation more precise. - There is no support yet for automatically defining the API of custom controls.
- The packages/ui-form/webapp directory contains the TypeScript implementation of the UI5 app. See the next section for details of a controller implementation inside this directory.
- The packages/ui-form/tsconfig.json file defines TypeScript compiler options like the JavaScript target version, the location of the
*.d.ts
files and the source and target directory for the TypeScript compilation. - The packages/ui-form/.babelrc.json file controls the build steps (first TypeScript to ES6 ("modern" JavaScript), then the conversion of some ES6 language constructs (module imports, classes) to the UI5 way of resource loading and class definition)
- The packages/ui-form/package.json file contains the
build:ts
andwatch:ts
scripts for yarn which trigger TypeScript compilation and live serving (used from within yarn scripts inside the top-level package.json file).
Most of the application logic is implemented in the Registration.controller.ts file. For a general explanation of this file, please check the documentation in the main branch of this repository, which describes the JavaScript version of the controller in detail. The logic is the same in TypeScript and most of the code as well - after all TypeScript is a superset of JavaScript which "only" adds type information. So let's look at the differences only:
The controller file starts with code not typically seen in UI5 apps: importing modules. But this is not TypeScript-specific at all, it's just the modern JavaScript we want to promote:
import Controller from "sap/ui/core/mvc/Controller";
import MessageBox from "sap/m/MessageBox";
import MessageToast from "sap/m/MessageToast";
...
This part is transformed to the well-known sap.ui.define(...)
or sap.ui.require(...)
in addition to the TypeScript compilation.
The next section consists of pure TypeScript: the definition of certain structures used within the controller:
type Person = {
LastName: string,
FirstName: string,
Birthday: string
};
...
Not all apps use structures like these. But if they do, defining the types provides all the type safety and code completion goodies which TypeScript is good for.
Then, the controller class is defined, inheriting from sap.ui.core.mvc.Controller
. This is again just modern JavaScript instead of the UI5-proprietary Controller.extend(...)
. Also how the member methods like onInit()
and the private member variables like oBundle
are defined has nothing to do with TypeScript. The only pieces of TypeScript in this section of the controller code are how the private member variables and the onInit
method are typed (e.g. oBundle
is defined to be of type ResourceBundle
).
/**
* @namespace sap.ui.eventregistration.form.controller
*/
export default class Registration extends Controller {
private bundle : ResourceBundle;
private oDataModel : ODataModel;
public onInit() : void {
Within the actual controller implementation, there is very little TypeScript-specific code!
TypeScript code is found for example after method parameters: they need to be typed explicitly - here aContexts
is an array of OData V4 contexts:
public onExistingDataLoaded(contexts : V4Context[]) : void {
...
}
Local variables are sometimes implicitly typed via the assigned value (this.bundle
has type ResourceBundle
, so TypeScript knows this is also the type of the local variable bundle
):
const bundle = this.bundle;
But sometimes the type is specified explicitly, e.g. when there is no immediate assignment to the newly declared variable:
let prop : PersonProp;
In some places there is a typecast using the "as
" keyword, mostly when calling getters like getModel()
which returns the superclass sap.ui.model.Model
(the actual instance may be a ResourceModel
or an ODataModel
). Or byId(...)
, which returns sap.ui.core.Element
, while the returned element may - depending on the ID - be a sap.m.Button
:
this.oDataModel = this.getOwnerComponent().getModel() as ODataModel;
...
(this.byId("submitButton") as Button).setEnabled(true);
NOTE: Many typecasts like the first one here will become unnecessary once these getter methods make use of generics (this is work in progress). Then it will be sufficient to specify the type of the left-hand side variable (here: this.oDataModel
).
Actually, that's it!
As of writing, the entire controller implementation is almost pure JavaScript with only a dozen typecasts and a dozen type definitions for variables as TypeScript-specific code! So TypeScript is not a new language on its own, but merely an addition which may come with minimal effort, but lots of benefits.
Please refer to the TypeScript Hello World app and in particular to the step-by-step setup explanation to understand the overall project setup for developing TypeScript-based UI5 apps.
You can also use that app as copy template for getting started quickly.
- Before writing UI5 application code, learn the fundamentals of TypeScript. The official webpage of the language contains a "handbook" and even a quick 5-minutes introduction for JavaScript developers which will help.
- Then, for an impression how UI5 app code written in TypeScript looks, check out the section above which walks you through the Registration controller code.
- Then - code! Import modules and define classes using the ES6 syntax. Write the rest of the code like you would normally do in JavaScript! Whenever you need to do something for TypeScript, an error message will tell you! (this requires one of the many TypeScript-aware code editors like VSCode)
These are the errors you will encounter most often - and how to solve them.
When TypeScript cannot find out the type of a variable on its own, specify it:
Solution:
public onExistingDataLoaded(contexts : V4Context[]) {
When TypeScript complains about a type missing certain properties, it means that the types do not match. This even happens sometimes when a typecast to a specific subclass is required:
Solution:
const oDataModel : ODataModel = this.getOwnerComponent().getModel() as ODataModel;
When you are missing a certain mathod in code completion or you try to call this method and TypeScript complains about the method not existing, it may be needed to cast to the specific type you are using:
Solution:
(this.byId("submitButton") as Button).setEnabled(true);
Most other errors you will encounter might actually point you to real issues in the code! That's what TypeScript is for.
Now enjoy also the other benefits of TypeScript, like the code completion and inline documentation, e.g. for constructors:
...or for method calls:
-
When you import the
sap.ui.core.Core
class, you get the singleton instance of the core, just like when you usesap.ui.require(...)
:import Core from "sap/ui/core/Core"; ... Core.byId("myButton");
-
Make sure to avoid collisions. The type
Event
is already defined as browser event, so you might want to locally name itUI5Event
:import UI5Event from "sap/ui/base/Event";
-
Often there is a 1:1 relationship between the classes you want to use and the modules to load, so most imports in UI5 apps will look like this, loading a class which is the default export of a module:
import Button from "sap/m/Button";
But there are also modules with multiple exports where you may want to pick a specific one:
import { URLHelper } from "sap/m/library"; import { CSSColor, ValueState, SortOrder } from "sap/ui/core/library"; import MessageBox, { Action as MessageBoxAction } from "sap/m/MessageBox"; import { support } from "sap/ui/Device";
This is mostly the case for all types defined in library.js files (e.g. many of the enums) and also enums defined within controls.
And of course there are also modules (especially in low-level parts of the core) which define functions to use:import syncStyleClass from "sap/ui/core/syncStyleClass";
-
If you need to use jQuery directly, you can import it like this:
import jQuery from "sap/ui/thirdparty/jquery";
You can run a TypeScript check of the app from the command line with:
yarn ts-typecheck
This will run a test compilation of the app and output any TypeScript errors.
You can also lint the TypeScript code with:
yarn lint
As the regular documentation explains, you can run the app in development ("watch") mode with:
yarn start
or if you only want to run the ui-form app:
yarn start:ui-form
As the regular documentation explains, you can build the app with:
yarn start
This includes the TypeScript compilation.
In the debugger of browsers, you can step through the original TypeScript code you wrote:
This is achieved by generating sourcemaps, which contain the original code and information to which place in that original code the actually executed JavaScript statements belong. At least in Chrome, the TypeScript version of the code is automatically opened when a breakpoint is hit, even when that breakpoint was set in the JavaScript version of a file. If you can't see the TypeScript code, make sure sourcemaps are enabled in the settings of your browser's developer tools.
One concern when using TypeScript might be whether runtime performance or code size is affected by using TypeScript instead of JavaScript. The short anwer is: no.
The slightly longer answer is: the browser anyway executes the compiled JavaScript, not TypeScript. And the compilation for the most part just removes the type information, so the compiled code is pretty much the same as when you write JavaScript directly. No size penalty, no additional logic. This is not an exhaustive answer, just a rule of thumb.
For example, as of writing, the Registration controller has 265 lines of code in TypeScript, which are compiled to 226 lines of JavaScript (without minification). The controller written in native JavaScript has 230 lines of code.
The compilation performance should not be a concern because it is barely noticeable in small to medium projects. If it becomes one in huge projects, there are certain hints available how to lower compilation time.
Remember that TypeScript does NOT ensure type correctness at runtime. Despite all compile-time checks, wrongly typed values can still sneak in at runtime, e.g. from JSON API calls to external systems which return an unexpected structure. Those will then lead to the same kind of issues occurring in regular JavaScript apps. Any runtime type checks you want need to be done by you.
Let's say you have an existing JavaScript UI5 app and want to convert it to TypeScript: what are the steps and efforts?
The general process is to walk through the below steps, guided by the TypeScript errors displayed in the editor (and reported by running yarn verify-app
).
These steps are not really related to TypeScript, but convert the code into the more modern JavaScript syntax suppoted by the UI5 type definition files.
The first step is to convert class definitions from the proprietary UI5 syntax to ES class syntax.
From:
var App = Controller.extend("ui5tssampleapp.controller.App", {
onInit: function _onInit() {
// apply content density mode to root view
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
}
});
To:
/**
* @namespace ui5tssampleapp.controller
*/
class App extends Controller {
public onInit() {
// apply content density mode to root view
this.getView().addStyleClass((this.getOwnerComponent()).getContentDensityClass());
};
};
It is important to annotate the class with the namespace, so the back transformation can re-add it.
The second step is to convert the dependency loading (sap.ui.require(...)
) to ES module syntax. Many of the JS files in a typical UI5 app actually do not only require dependencies, but also provide a new class on their own - often a controller. In this case sap.ui.define(...)
is replaced with ES module imports and a module export.
In the above example, this looks as follows.
Before:
sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
/**
* @namespace ui5tssampleapp.controller
*/
class App extends Controller {
... // as above
};
return App;
});
After:
import Controller from "sap/ui/core/mvc/Controller";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
... // as above
};
- Add type information to method parameters.
- Add private member class variables (with type information) to the beginning of the class definition. (In JavaScript they are often created on-the-fly later on during the lifetime of a class instance.)
- Convert conventional functions to arrow functions when
someFunction.bind(...)
is used because TypeScript does not seem to propagate the type of the bound "this" context into the function body. - Define further types and structures needed withing the code, if applicable.
Hint: use the most precise type to have all properties available. Examples:
- Use specific types like
KeyboardEvent
, not justEvent
for browser events. - Use
JQuery.DropEvent
, notJQuery.Event
orJQueryEventObject
or justEvent
whenevent.originalTarget
is needed. - Use
JQueryXHR
, notXMLHttpRequest
. - ...
Generic getter methods like document.getElementById(...)
are commonly defined to return the super-type of all possible types (in this case HTMLElement
) although in practice it will usually be a specific sub-type (e.g. an HTMLAnchorElement
).
In many cases you will have to cast the return value to the specific type to use it.
The same is valid for several UI5 methods, most prominently the following:
- core.byId() / view.byId()
- control.getBinding()
- ownerComponent.getModel()
- event.getSource()
- component.getRootControl()
- this.getOwnerComponent()
This cast will sometimes also require an additional module import to make the type known. Sometimes this will be offered as "quick fix" by the code editor, sometimes it will have to be done manually.
Coming back to the app controller example used above, this step will complete the TypeScript conversion: an additional import of the app's component is needed (called AppComponent
), so within the onInit
implementation the required typecast can be done. Without this typecast, the return type of getOwnerComponent
would be a sap.ui.core.Component
, which does not have the getContentDensityClass
method defined in the app component.
Before:
import Controller from "sap/ui/core/mvc/Controller";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
public onInit() {
// apply content density mode to root view
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
};
};
After:
import Controller from "sap/ui/core/mvc/Controller";
import AppComponent from "../Component";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
public onInit() : void {
// apply content density mode to root view
this.getView().addStyleClass((this.getOwnerComponent() as AppComponent).getContentDensityClass());
};
};
For this controller file, the TypeScript conversion is now complete (and the actual TypeScript part of the conversion is the typecast).
(Note: the "void" definition of the method return type is not strictly demanded by TypeScript, but by the current linting settings.)
At this point, the number of remaining TypeScript errors should be reduced to a small minority.
Some of the limitations listed above can lead to TypeScript errors which are left after the above steps. E.g. when some complex types in method return values are not completely defined. This means you will not get code completion nor a type for these values. You are welcome to report an issue at GitHub to let us know, so we can improve the documentation. The same is recommended when a method has parameters which are not marked as optional, but can be omitted.
From our own app conversions for testing, we can say that the effort for converting small apps with 5-6 views and controllers to TypeScript is really limited (few hours).
While doing so, we have found (and fixed) a few places in the UI5 type definitions which were not 100% defined (usually no problem, as easy workarounds like explicitly giving a type are possible), but we have also found a few actual bugs in the application code, e.g. calling methods with wrong or missing parameters, access to private methods/variables of controls, or just typos.
There has been a preview on TypeScript in a "UI5ers live" web conference. You can access the recording on YouTube.
The UI5 TypeScript Hello World app is a great place to learn how the project setup works.