Skip to content
61 changes: 61 additions & 0 deletions packages/dts-generator/src/resources/typed-json-model.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
declare module "sap/ui/model/json/TypedJSONModel" {
import Filter from "sap/ui/model/Filter";
import Sorter from "sap/ui/model/Sorter";
import JSONModel from "sap/ui/model/json/JSONModel";
import JSONListBinding from "sap/ui/model/json/JSONListBinding";
import TypedJSONContext from "sap/ui/model/json/TypedJSONContext";
import Context from "sap/ui/model/Context";

Expand Down Expand Up @@ -30,6 +33,24 @@ declare module "sap/ui/model/json/TypedJSONModel" {
oContext: TypedJSONContext<Data, Root>,
): PropertyByRelativeBindingPath<Data, Root, Path>;

bindList<Path extends AbsoluteListBindingPath<Data>>(
sPath: Path,
oContext?: undefined,
aSorters?: Sorter | Sorter[],
aFilters?: Filter | Filter[],
mParameters?: object,
): JSONListBinding;
bindList<
Path extends RelativeListBindingPath<Data, Root>,
Root extends AbsoluteBindingPath<Data>,
>(
sPath: Path,
oContext?: TypedJSONContext<Data, Root>,
aSorters?: Sorter | Sorter[],
aFilters?: Filter | Filter[],
mParameters?: object,
): JSONListBinding;

setData(oData: Data, bMerge?: boolean): void;

// setProperty with AbsoluteBindingPath (context === undefined),
Expand Down Expand Up @@ -82,6 +103,25 @@ declare module "sap/ui/model/json/TypedJSONModel" {
: // if T is not of type object:
never;

/**
* Valid absolute binding path for underlying `Array` types.
*
* @example
* type SalesOrder = { id: string, items: string[] };
* type PathInObject = PathInJSONModel<SalesOrder>; // "/id" | "/items"
* let path: PathInObject = "/items"; // ok
* path = "/id"; // error
* path = "/items/0"; // error, since an element in the array is a string
*/
export type AbsoluteListBindingPath<Type> = {
[Path in AbsoluteBindingPath<Type>]: PropertyByAbsoluteBindingPath<
Type,
Path
> extends Array<unknown>
? Path
: never;
}[AbsoluteBindingPath<Type>];

/**
* Valid relative binding path in a JSONModel.
* The root of the path is defined by the given root string.
Expand All @@ -98,6 +138,27 @@ declare module "sap/ui/model/json/TypedJSONModel" {
? Rest
: never;

/**
* Valid relative binding path for underlying `Array` types.
* The root of the path is defined by the given root string.
*
* @example
* type SalesOrder = { buyer: { id: string, items: string[] } };
* type PathRelativeToSalesOrder = RelativeListBindingPath<SalesOrderWrapper, "/buyer">; // "id" | "items"
*/
export type RelativeListBindingPath<
Type,
Root extends AbsoluteBindingPath<Type>,
> = {
[Path in RelativeBindingPath<Type, Root>]: PropertyByRelativeBindingPath<
Type,
Root,
Path
> extends Array<unknown>
? Path
: never;
}[RelativeBindingPath<Type, Root>];

/**
* The type of a property in a JSONModel identified by the given path.
* Counterpart to {@link AbsoluteBindingPath}.
Expand Down
35 changes: 35 additions & 0 deletions test-packages/typed-json-model/webapp/model/model.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import Context from "sap/ui/model/Context";
import Filter from "sap/ui/model/Filter";
import Sorter from "sap/ui/model/Sorter";
import JSONModel from "sap/ui/model/json/JSONModel";
import JSONListBinding from "sap/ui/model/json/JSONListBinding";
import {
AbsoluteBindingPath,
AbsoluteListBindingPath,
PropertyByAbsoluteBindingPath,
PropertyByRelativeBindingPath,
RelativeBindingPath,
RelativeListBindingPath,
} from "./typing";

export class TypedJSONContext<Data extends object, Root extends AbsoluteBindingPath<Data>> extends Context {
Expand Down Expand Up @@ -57,6 +62,36 @@ export class TypedJSONModel<Data extends object> extends JSONModel {
| PropertyByRelativeBindingPath<Data, Root, Path>;
}

// Overload for absolute paths
bindList<Path extends AbsoluteListBindingPath<Data>>(
sPath: Path,
oContext?: undefined,
aSorters?: Sorter | Sorter[],
aFilters?: Filter | Filter[],
mParameters?: object,
): JSONListBinding;
// Overload for relative paths
bindList<Path extends RelativeListBindingPath<Data, Root>, Root extends AbsoluteBindingPath<Data>>(
sPath: Path,
oContext: TypedJSONContext<Data, Root>,
aSorters?: Sorter | Sorter[],
aFilters?: Filter | Filter[],
mParameters?: object,
): JSONListBinding;
// Implementation
bindList<
Path extends AbsoluteListBindingPath<Data> | RelativeListBindingPath<Data, Root>,
Root extends AbsoluteBindingPath<Data>,
>(
sPath: Path,
oContext?: TypedJSONContext<Data, Root>,
aSorters?: Sorter | Sorter[],
aFilters?: Filter | Filter[],
mParameters?: object,
): JSONListBinding {
return super.bindList(sPath, oContext, aSorters, aFilters, mParameters);
}

setData(oData: Data, bMerge?: boolean): void {
super.setData(oData, bMerge);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { JSONSafe, objectLikeByInference, Placeholder } from "../input";
import JSONListBinding from "sap/ui/model/json/JSONListBinding";

import { TypedJSONModel } from "../../model";

Expand Down Expand Up @@ -76,3 +77,22 @@ import { TypedJSONModel } from "../../model";

/** @expect ts2740 */ const dataB: Array<any> = model.getData();
/** @expect ts2345 */ model.setData(dataB);

/***********************************************************************************************************************
* Check model.bindList
**********************************************************************************************************************/

/** @expect ok */ let listBinding: JSONListBinding = model.bindList("/anArray");
/** @expect ok */ listBinding = model.bindList("/anArrayOfArrays/0");
/** @expect ok */ listBinding = model.bindList("/anObjectWithArray/anArray");

// incorrect binding paths
/** @expect ts2345 */ listBinding = model.bindList("/aJsonSafeArray/0");
/** @expect ts2345 */ listBinding = model.bindList("/anArrayOfArrays/0/0");
/** @expect ts2345 */ listBinding = model.bindList("/anObjectWithArray/anArray/0");
/** @expect ts2345 */ listBinding = model.bindList("/anArrayOfPlaceholders/0");
/** @expect ts2345 */ listBinding = model.bindList("/anArrayOfObjects/0");

// bindList always returns a JSONListBinding and cannot be assigned to other types
/** @expect ts2739 */ aPlaceholder = model.bindList("/anArray");
/** @expect ts2322 */ aJsonSafe = model.bindList("/anArray");
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { IObjectLike, JSONSafe, objectLikeByInterface, Placeholder, TObjectLike } from "../input";
import JSONListBinding from "sap/ui/model/json/JSONListBinding";

import { TypedJSONModel } from "../../model";

Expand Down Expand Up @@ -81,3 +82,22 @@ import { TypedJSONModel } from "../../model";

/** @expect ts2740 */ const dataC: Array<any> = model.getData();
/** @expect ts2345 */ model.setData(dataC);

/***********************************************************************************************************************
* Check model.bindList
**********************************************************************************************************************/

/** @expect ok */ let listBinding: JSONListBinding = model.bindList("/anArray");
/** @expect ok */ listBinding = model.bindList("/anArrayOfArrays/0");
/** @expect ok */ listBinding = model.bindList("/anObjectWithArray/anArray");

// incorrect binding paths
/** @expect ts2345 */ listBinding = model.bindList("/aJsonSafeArray/0");
/** @expect ts2345 */ listBinding = model.bindList("/anArrayOfArrays/0/0");
/** @expect ts2345 */ listBinding = model.bindList("/anObjectWithArray/anArray/0");
/** @expect ts2345 */ listBinding = model.bindList("/anArrayOfPlaceholders/0");
/** @expect ts2345 */ listBinding = model.bindList("/anArrayOfObjects/0");

// bindList always returns a JSONListBinding and cannot be assigned to other types
/** @expect ts2739 */ aPlaceholder = model.bindList("/anArray");
/** @expect ts2322 */ aJsonSafe = model.bindList("/anArray");
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { IObjectLike, JSONSafe, objectLikeByTypeAlias, Placeholder, TObjectLike } from "../input";
import JSONListBinding from "sap/ui/model/json/JSONListBinding";

import { TypedJSONModel } from "../../model";

Expand Down Expand Up @@ -81,3 +82,22 @@ import { TypedJSONModel } from "../../model";

/** @expect ts2740 */ const dataC: Array<any> = model.getData();
/** @expect ts2345 */ model.setData(dataC);

/***********************************************************************************************************************
* Check model.bindList
**********************************************************************************************************************/

/** @expect ok */ let listBinding: JSONListBinding = model.bindList("/anArray");
/** @expect ok */ listBinding = model.bindList("/anArrayOfArrays/0");
/** @expect ok */ listBinding = model.bindList("/anObjectWithArray/anArray");

// incorrect binding paths
/** @expect ts2345 */ listBinding = model.bindList("/aJsonSafeArray/0");
/** @expect ts2345 */ listBinding = model.bindList("/anArrayOfArrays/0/0");
/** @expect ts2345 */ listBinding = model.bindList("/anObjectWithArray/anArray/0");
/** @expect ts2345 */ listBinding = model.bindList("/anArrayOfPlaceholders/0");
/** @expect ts2345 */ listBinding = model.bindList("/anArrayOfObjects/0");

// bindList always returns a JSONListBinding and cannot be assigned to other types
/** @expect ts2739 */ aPlaceholder = model.bindList("/anArray");
/** @expect ts2322 */ aJsonSafe = model.bindList("/anArray");
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { JSONSafe, objectLikeByInference, Placeholder } from "../input";
import JSONListBinding from "sap/ui/model/json/JSONListBinding";

import { TypedJSONModel } from "../../model";

Expand Down Expand Up @@ -67,3 +68,22 @@ import { TypedJSONModel } from "../../model";
/** @expect ts2322 */ aJsonSafe = model.getProperty("anArrayOfPlaceholders/0", context);
/** @expect ts2322 */ anElementInATuple = model.getProperty("aTuple", context);
/** @expect ts2322 */ anObject = model.getProperty("aTuple/0", context);

/***********************************************************************************************************************
* Check model.bindList
**********************************************************************************************************************/

/** @expect ok */ let listBinding: JSONListBinding = model.bindList("anArray", context);
/** @expect ok */ listBinding = model.bindList("anArrayOfArrays/0", context);
/** @expect ok */ listBinding = model.bindList("anObjectWithArray/anArray", context);

// incorrect binding paths
/** @expect ts2769 */ listBinding = model.bindList("aJsonSafeArray/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anArrayOfArrays/0/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anObjectWithArray/anArray/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anArrayOfPlaceholders/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anArrayOfObjects/0", context);

// bindList always returns a JSONListBinding and cannot be assigned to other types
/** @expect ts2739 */ aPlaceholder = model.bindList("anArray", context);
/** @expect ts2322 */ aJsonSafe = model.bindList("anArray", context);
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { JSONSafe, objectLikeByInterface, Placeholder } from "../input";
import JSONListBinding from "sap/ui/model/json/JSONListBinding";

import { TypedJSONModel } from "../../model";

Expand Down Expand Up @@ -67,3 +68,22 @@ import { TypedJSONModel } from "../../model";
/** @expect ts2322 */ aJsonSafe = model.getProperty("anArrayOfPlaceholders/0", context);
/** @expect ts2322 */ anElementInATuple = model.getProperty("aTuple", context);
/** @expect ts2322 */ anObject = model.getProperty("aTuple/0", context);

/***********************************************************************************************************************
* Check model.bindList
**********************************************************************************************************************/

/** @expect ok */ let listBinding: JSONListBinding = model.bindList("anArray", context);
/** @expect ok */ listBinding = model.bindList("anArrayOfArrays/0", context);
/** @expect ok */ listBinding = model.bindList("anObjectWithArray/anArray", context);

// incorrect binding paths
/** @expect ts2769 */ listBinding = model.bindList("aJsonSafeArray/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anArrayOfArrays/0/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anObjectWithArray/anArray/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anArrayOfPlaceholders/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anArrayOfObjects/0", context);

// bindList always returns a JSONListBinding and cannot be assigned to other types
/** @expect ts2739 */ aPlaceholder = model.bindList("anArray", context);
/** @expect ts2322 */ aJsonSafe = model.bindList("anArray", context);
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { JSONSafe, objectLikeByTypeAlias, Placeholder } from "../input";
import JSONListBinding from "sap/ui/model/json/JSONListBinding";

import { TypedJSONModel } from "../../model";

Expand Down Expand Up @@ -69,3 +70,22 @@ model.getProperty("/root/aPlaceholder", context);
/** @expect ts2322 */ aJsonSafe = model.getProperty("anArrayOfPlaceholders/0", context);
/** @expect ts2322 */ anElementInATuple = model.getProperty("aTuple", context);
/** @expect ts2322 */ anObject = model.getProperty("aTuple/0", context);

/***********************************************************************************************************************
* Check model.bindList
**********************************************************************************************************************/

/** @expect ok */ let listBinding: JSONListBinding = model.bindList("anArray", context);
/** @expect ok */ listBinding = model.bindList("anArrayOfArrays/0", context);
/** @expect ok */ listBinding = model.bindList("anObjectWithArray/anArray", context);

// incorrect binding paths
/** @expect ts2769 */ listBinding = model.bindList("aJsonSafeArray/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anArrayOfArrays/0/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anObjectWithArray/anArray/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anArrayOfPlaceholders/0", context);
/** @expect ts2769 */ listBinding = model.bindList("anArrayOfObjects/0", context);

// bindList always returns a JSONListBinding and cannot be assigned to other types
/** @expect ts2739 */ aPlaceholder = model.bindList("anArray", context);
/** @expect ts2322 */ aJsonSafe = model.bindList("anArray", context);
19 changes: 19 additions & 0 deletions test-packages/typed-json-model/webapp/model/test/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export interface IPrimitives {
export type TObjectLike = {
anObject: object;
anArray: Array<unknown>;
anArrayOfArrays: Array<Array<unknown>>;
aJsonSafeArray: Array<JSONSafe>;
anObjectWithArray: { anArray: Array<unknown> };
anArrayOfPlaceholders: Array<Placeholder>;
aPlaceholder: Placeholder;
aTuple: [string, number];
Expand All @@ -71,7 +73,9 @@ export type TObjectLike = {
export interface IObjectLike {
anObject: object;
anArray: Array<unknown>;
anArrayOfArrays: Array<Array<unknown>>;
aJsonSafeArray: Array<JSONSafe>;
anObjectWithArray: { anArray: Array<unknown> };
anArrayOfPlaceholders: Array<Placeholder>;
aPlaceholder: Placeholder;
aTuple: [string, number];
Expand Down Expand Up @@ -123,8 +127,13 @@ export const primitivesByInference = {
export const objectLikeByTypeAlias: TObjectLike = {
anObject: {},
anArray: [],
anArrayOfArrays: [
["string", 1],
[true, false],
],
aJsonSafeArray: ["string", 1, true],
anArrayOfPlaceholders: [new Placeholder()],
anObjectWithArray: { anArray: ["string"] },
aPlaceholder: new Placeholder(),
aTuple: ["string", 1],
};
Expand All @@ -136,8 +145,13 @@ export const objectLikeByTypeAlias: TObjectLike = {
export const objectLikeByInterface: IObjectLike = {
anObject: {},
anArray: [],
anArrayOfArrays: [
["string", 1],
[true, false],
],
aJsonSafeArray: ["string", 1, true],
anArrayOfPlaceholders: [new Placeholder()],
anObjectWithArray: { anArray: ["string"] },
aPlaceholder: new Placeholder(),
aTuple: ["string", 1],
};
Expand All @@ -149,8 +163,13 @@ export const objectLikeByInterface: IObjectLike = {
export const objectLikeByInference = {
anObject: {},
anArray: [],
anArrayOfArrays: [
["string", 1],
[true, false],
],
aJsonSafeArray: ["string", 1, true],
anArrayOfObjects: [{ aNumber: 1 }],
anObjectWithArray: { anArray: ["string"] },
anArrayOfPlaceholders: [new Placeholder()],
aPlaceholder: new Placeholder(),
aTuple: ["string", 1],
Expand Down
Loading
Loading