TypedJSONModel Knows Its Own Shape
The path in JSONModel.getProperty("/planning/tasks/0/estimateHours") has always been a string. A plain sap.ui.model.json.JSONModel accepts any string there. TypeScript can see that getProperty expects a string, but it has no model-data shape to compare that string against. A typo returns undefined, the table renders empty, and the bug lives until someone notices the missing column. The model behaves the same way whether the surrounding project is written in JavaScript or TypeScript.
sap.ui.model.json.TypedJSONModel and sap.ui.model.json.TypedJSONContext change that for TypeScript projects. They were introduced in SAPUI5 1.140 as strongly-typed wrappers around JSONModel and Context.1 Selected model methods get stricter TypeScript signatures, and path strings are checked against the data type you give the model.2
Defining the model shape
The data type can be inferred from the constructor or declared explicitly. With constructor inference, the model shape comes from the object you pass in:
import TypedJSONModel from "sap/ui/model/json/TypedJSONModel";
const data = {
planning: {
releaseId: "REL-2026-06",
locked: false,
tasks: [
{
taskId: "TASK-17",
title: "Review checkout flow",
estimateHours: 6,
done: false,
},
],
},
};
const model = new TypedJSONModel(data);
TypeScript infers the model's shape from data. This is enough for view models that are built locally and never reshaped.
When the field is declared before it is filled, the generic has to be written out. A controller that creates the model later is the usual case:
import Controller from "sap/ui/core/mvc/Controller";
import TypedJSONModel from "sap/ui/model/json/TypedJSONModel";
interface PlanningBoard {
releaseId: string;
locked: boolean;
tasks: PlanningTask[];
}
interface PlanningTask {
taskId: string;
title: string;
estimateHours: number;
done: boolean;
}
export default class App extends Controller {
private model!: TypedJSONModel<{ planning: PlanningBoard }>;
}
From that point on, model.getProperty("/planning/tasks/0/estimateHours") returns number, and TypeScript reports a type-checking error for a path that does not exist in the declared shape.
What TypeScript catches
The typed getProperty and setProperty carry both pieces that plain JSONModel lacks: the allowed path and the value type at that path.
const estimate = model.getProperty("/planning/tasks/0/estimateHours"); // number
const task = model.getProperty("/planning/tasks/0"); // PlanningTask
const board = model.getProperty("/planning"); // PlanningBoard
model.getProperty("/planning/tasks/0/estimateHour");
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// `estimateHours` exists on PlanningTask. `estimateHour` does not.
model.setProperty("/planning/locked", "true");
// ~~~~~~
// Argument of type 'string' is not assignable to parameter of type 'boolean'.
model.setProperty("/planning/tasks/0/estimateHours", 0);
// ok
The first call fails before runtime because the property name is wrong. The second fails because /planning/locked points to a boolean property, but the value passed to setProperty is a string. The successful call shows the other half of the feature: once the path is valid, the value has to match the property behind it.
The same constraint applies to createBindingContext, getMessagesByPath, and the binding factory methods bindList, bindProperty, bindContext, and bindTree in the type definitions.3 bindList only accepts paths whose target type is an array. bindContext only accepts paths whose target type is an object. TypeScript enforces the call-shape, not the framework.
Relative paths through TypedJSONContext
createBindingContext returns a TypedJSONContext<Data, Root>, where Root is the absolute path that produced the context. From that point on, getProperty on the context accepts only the relative paths that are valid under that root:
const planningContext = model.createBindingContext("/planning");
const releaseId = planningContext.getProperty("releaseId"); // string
const firstTitle =
planningContext.getProperty("tasks/0/title"); // string
planningContext.getProperty("tasks/0/estimateHour");
// ~~~~~~~~~~~~~~~~~~~~~~~~
// Under /planning, the relative path is tasks/0/estimateHours.
The same context can be passed back into the model:
model.setProperty("locked", false, planningContext);
model.getProperty("tasks/0/estimateHours", planningContext); // number
Controllers that hold one context per row, or one context per detail screen, get a typed root with the rest of the paths checked against it. The relative form is what most production controller code reaches for, so this is the part that pays back fastest.
Where the typing ends
The feature is narrower than it sounds, and the places it does not reach matter:
- XML view binding paths are still outside this type check.
text="{/planning/tasks/0/title}"in an XML view is parsed as XML view binding syntax, not as a TypeScript call toTypedJSONModel.getProperty. The SAPUI5 guide lists the typed JavaScript/TypeScript APIs, and the SAPUI5 Language Assistant's XML completion package documents completion for XML tags, attributes, namespaces, and binding-info syntax rather than JSON data paths derived from a TypeScript generic.24 In Visual Studio Code or SAP Business Application Studio, you can still get XML view assistance. What you should not expect fromTypedJSONModelis autocomplete or type checking for/planning/tasks/...inside XML. - The data type describes what you claim, not what the server returned. Calling
setDatawith the right shape and then receiving badly-shaped JSON over HTTP does not produce a TypeScript error. The model trusts the type assertion. For backend validation, this is not a replacement. - Deep nesting hits a TypeScript ceiling. Every additional level adds more possible path strings for TypeScript to track. The UI5 TypeScript repository includes a limitation example where a ten-property object can be nested five times, but the sixth level trips TypeScript's
ts2590: Expression produces a union type that is too complex to representerror.5 The exact ceiling depends on width as well as depth. Wide, flat data shapes survive easily; deeply nested mirror-of-backend models do not. - Arrays that mix objects and primitives break inference. The limitation tests include an array whose elements can be either an object or a primitive value. In that shape, a lookup that reads a property from the object branch can make TypeScript infer
neverinstead of the property type.5neveris a real TypeScript type, used for values that cannot occur; it is not a JavaScript runtime value. Heterogeneous object arrays fare better, but object-plus-primitive arrays are a bad fit for this type machinery. - Available only when the typings are installed. The runtime class is in SAPUI5 1.140 and later, but the generic and the path-extraction machinery ship in
@sapui5/types/@openui5/types. JavaScript projects, and TypeScript projects without those typings, get the runtime subclass and none of the path checking.
None of this makes the feature unattractive. It does mean the value is concentrated where the typing actually fires: TypeScript controllers and helpers calling getProperty, setProperty, and the various bind* methods on a JSONModel whose shape is known at compile time.
What is actually shipped
At runtime, TypedJSONModel and TypedJSONContext are almost boring. The OpenUI5 implementation extends JSONModel and Context without adding methods or overriding behavior.6 That is the point behind SAP's wording that the wrappers do not affect runtime behavior and are not available for JavaScript projects.12
The interesting code lives in the UI5 TypeScript types shipped through @sapui5/types and @openui5/types. The relevant signatures in typed-json-model.d.ts look roughly like this:
class TypedJSONModel<Data extends object> extends JSONModel {
constructor(data?: Data, observe?: boolean);
getProperty<Path extends AbsoluteBindingPath<Data>>(
path: Path,
): PropertyByAbsoluteBindingPath<Data, Path>;
setProperty<Path extends AbsoluteBindingPath<Data>>(
path: Path,
value: PropertyByAbsoluteBindingPath<Data, Path>,
context?: undefined,
asyncUpdate?: boolean,
): boolean;
createBindingContext<Path extends AbsoluteBindingPath<Data>>(
path: Path,
...
): TypedJSONContext<Data, Path>;
// getData, setData, getMessagesByPath,
// bindContext, bindList, bindProperty, bindTree, ...
}
AbsoluteBindingPath<Data> is a recursive template literal type that expands to the union of legal /a/b/0/c-style paths inside Data. PropertyByAbsoluteBindingPath<Data, Path> walks the same structure to return the type at that path. The TypeScript type checker does the rest.3
Closing
TypedJSONModel is the rare UI5 feature that adds nothing at runtime and still pays back. The author of a controller does not have to invent a parallel typed wrapper, document path conventions, or write a layer of helpers to keep paths and value types in sync. Two small subclasses and a generous .d.ts make the existing JSONModel API check its own paths.
For TypeScript UI5 projects, the cost of switching is the import name. The reason not to switch is mostly the nesting-depth ceiling, and that limit is in TypeScript itself, not in UI5. Everything else is upside.
Sources
Footnotes
-
SAPUI5 SDK, What's New in SAPUI5 1.140: introduces
sap.ui.model.json.TypedJSONModelandsap.ui.model.json.TypedJSONContextand states that the wrappers do not affect runtime behavior. ↩ ↩2 -
SAPUI5 SDK, The JSON Model: Using the Typed JSON Model: documents the TypeScript-only scope, the typed
getData,setData,getProperty,setProperty, andcreateBindingContextmethods, and the typed context methods. ↩ ↩2 ↩3 -
UI5/typescript,
typed-json-model.d.tsin the dts-generator: definesAbsoluteBindingPath,RelativeBindingPath,PropertyByAbsoluteBindingPath, and the generic overloads. ↩ ↩2 -
SAP, UI5 Language Assistant: XML Views Completion: documents XML view completion for tag names, attribute names, and namespace prefixes. SAP,
property-binding-info.test.ts: the binding completion tests cover binding-info syntax and expect no completion for simple binding paths. ↩ -
UI5/typescript,
limitations.tsin the typed-json-model test package: documents the TypeScript-side depth ceiling (ts2590) and the mixed object-plus-primitive array inference break. ↩ ↩2 -
OpenUI5 source,
TypedJSONModel.jsandTypedJSONContext.jsin 1.147.1: both classes are smallextendcalls with empty prototypes. ↩