diff --git a/packages/dts-generator/src/resources/typed-json-model.d.ts b/packages/dts-generator/src/resources/typed-json-model.d.ts index 5d33d33..25a5878 100644 --- a/packages/dts-generator/src/resources/typed-json-model.d.ts +++ b/packages/dts-generator/src/resources/typed-json-model.d.ts @@ -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"; @@ -30,6 +33,24 @@ declare module "sap/ui/model/json/TypedJSONModel" { oContext: TypedJSONContext, ): PropertyByRelativeBindingPath; + bindList>( + sPath: Path, + oContext?: undefined, + aSorters?: Sorter | Sorter[], + aFilters?: Filter | Filter[], + mParameters?: object, + ): JSONListBinding; + bindList< + Path extends RelativeListBindingPath, + Root extends AbsoluteBindingPath, + >( + sPath: Path, + oContext?: TypedJSONContext, + aSorters?: Sorter | Sorter[], + aFilters?: Filter | Filter[], + mParameters?: object, + ): JSONListBinding; + setData(oData: Data, bMerge?: boolean): void; // setProperty with AbsoluteBindingPath (context === undefined), @@ -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; // "/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 = { + [Path in AbsoluteBindingPath]: PropertyByAbsoluteBindingPath< + Type, + Path + > extends Array + ? Path + : never; + }[AbsoluteBindingPath]; + /** * Valid relative binding path in a JSONModel. * The root of the path is defined by the given root string. @@ -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; // "id" | "items" + */ + export type RelativeListBindingPath< + Type, + Root extends AbsoluteBindingPath, + > = { + [Path in RelativeBindingPath]: PropertyByRelativeBindingPath< + Type, + Root, + Path + > extends Array + ? Path + : never; + }[RelativeBindingPath]; + /** * The type of a property in a JSONModel identified by the given path. * Counterpart to {@link AbsoluteBindingPath}. diff --git a/test-packages/typed-json-model/webapp/model/model.ts b/test-packages/typed-json-model/webapp/model/model.ts index a3b70ad..0b00cb5 100644 --- a/test-packages/typed-json-model/webapp/model/model.ts +++ b/test-packages/typed-json-model/webapp/model/model.ts @@ -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> extends Context { @@ -57,6 +62,36 @@ export class TypedJSONModel extends JSONModel { | PropertyByRelativeBindingPath; } + // Overload for absolute paths + bindList>( + sPath: Path, + oContext?: undefined, + aSorters?: Sorter | Sorter[], + aFilters?: Filter | Filter[], + mParameters?: object, + ): JSONListBinding; + // Overload for relative paths + bindList, Root extends AbsoluteBindingPath>( + sPath: Path, + oContext: TypedJSONContext, + aSorters?: Sorter | Sorter[], + aFilters?: Filter | Filter[], + mParameters?: object, + ): JSONListBinding; + // Implementation + bindList< + Path extends AbsoluteListBindingPath | RelativeListBindingPath, + Root extends AbsoluteBindingPath, + >( + sPath: Path, + oContext?: TypedJSONContext, + 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); } diff --git a/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-inference.ts b/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-inference.ts index 6a37cff..57d0a22 100644 --- a/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-inference.ts +++ b/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-inference.ts @@ -10,6 +10,7 @@ */ import { JSONSafe, objectLikeByInference, Placeholder } from "../input"; +import JSONListBinding from "sap/ui/model/json/JSONListBinding"; import { TypedJSONModel } from "../../model"; @@ -76,3 +77,22 @@ import { TypedJSONModel } from "../../model"; /** @expect ts2740 */ const dataB: Array = 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"); diff --git a/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-interface.ts b/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-interface.ts index 142b3b8..4162574 100644 --- a/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-interface.ts +++ b/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-interface.ts @@ -10,6 +10,7 @@ */ import { IObjectLike, JSONSafe, objectLikeByInterface, Placeholder, TObjectLike } from "../input"; +import JSONListBinding from "sap/ui/model/json/JSONListBinding"; import { TypedJSONModel } from "../../model"; @@ -81,3 +82,22 @@ import { TypedJSONModel } from "../../model"; /** @expect ts2740 */ const dataC: Array = 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"); diff --git a/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-typeAlias.ts b/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-typeAlias.ts index ab91bbc..41c7f2d 100644 --- a/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-typeAlias.ts +++ b/test-packages/typed-json-model/webapp/model/test/cases/absolute-complex-typeAlias.ts @@ -10,6 +10,7 @@ */ import { IObjectLike, JSONSafe, objectLikeByTypeAlias, Placeholder, TObjectLike } from "../input"; +import JSONListBinding from "sap/ui/model/json/JSONListBinding"; import { TypedJSONModel } from "../../model"; @@ -81,3 +82,22 @@ import { TypedJSONModel } from "../../model"; /** @expect ts2740 */ const dataC: Array = 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"); diff --git a/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-inference.ts b/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-inference.ts index b730283..eb30dc0 100644 --- a/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-inference.ts +++ b/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-inference.ts @@ -10,6 +10,7 @@ */ import { JSONSafe, objectLikeByInference, Placeholder } from "../input"; +import JSONListBinding from "sap/ui/model/json/JSONListBinding"; import { TypedJSONModel } from "../../model"; @@ -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); diff --git a/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-interface.ts b/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-interface.ts index 40236f3..5024a76 100644 --- a/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-interface.ts +++ b/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-interface.ts @@ -10,6 +10,7 @@ */ import { JSONSafe, objectLikeByInterface, Placeholder } from "../input"; +import JSONListBinding from "sap/ui/model/json/JSONListBinding"; import { TypedJSONModel } from "../../model"; @@ -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); diff --git a/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-typeAlias.ts b/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-typeAlias.ts index 8b0dedc..a97fed2 100644 --- a/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-typeAlias.ts +++ b/test-packages/typed-json-model/webapp/model/test/cases/relative-complex-typeAlias.ts @@ -10,6 +10,7 @@ */ import { JSONSafe, objectLikeByTypeAlias, Placeholder } from "../input"; +import JSONListBinding from "sap/ui/model/json/JSONListBinding"; import { TypedJSONModel } from "../../model"; @@ -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); diff --git a/test-packages/typed-json-model/webapp/model/test/input.ts b/test-packages/typed-json-model/webapp/model/test/input.ts index a8286f6..aac9fe7 100644 --- a/test-packages/typed-json-model/webapp/model/test/input.ts +++ b/test-packages/typed-json-model/webapp/model/test/input.ts @@ -58,7 +58,9 @@ export interface IPrimitives { export type TObjectLike = { anObject: object; anArray: Array; + anArrayOfArrays: Array>; aJsonSafeArray: Array; + anObjectWithArray: { anArray: Array }; anArrayOfPlaceholders: Array; aPlaceholder: Placeholder; aTuple: [string, number]; @@ -71,7 +73,9 @@ export type TObjectLike = { export interface IObjectLike { anObject: object; anArray: Array; + anArrayOfArrays: Array>; aJsonSafeArray: Array; + anObjectWithArray: { anArray: Array }; anArrayOfPlaceholders: Array; aPlaceholder: Placeholder; aTuple: [string, number]; @@ -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], }; @@ -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], }; @@ -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], diff --git a/test-packages/typed-json-model/webapp/model/typing.ts b/test-packages/typed-json-model/webapp/model/typing.ts index ba6d7f9..6613c88 100644 --- a/test-packages/typed-json-model/webapp/model/typing.ts +++ b/test-packages/typed-json-model/webapp/model/typing.ts @@ -28,6 +28,20 @@ export type AbsoluteBindingPath = : // 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; // "/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 = { + [Path in AbsoluteBindingPath]: PropertyByAbsoluteBindingPath extends Array ? Path : never; +}[AbsoluteBindingPath]; + /** * Valid relative binding path in a JSONModel. * The root of the path is defined by the given root string. @@ -39,6 +53,20 @@ export type AbsoluteBindingPath = export type RelativeBindingPath> = AbsoluteBindingPath> extends `/${infer Rest}` ? 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; // "id" | "items" + */ +export type RelativeListBindingPath> = { + [Path in RelativeBindingPath]: PropertyByRelativeBindingPath extends Array + ? Path + : never; +}[RelativeBindingPath]; + /** * The type of a property in a JSONModel identified by the given path. * Counterpart to {@link AbsoluteBindingPath}.