we are trying to have a very type strict frontend. Devextreme supports generic types for DataSource, however it is not validated at all.
Issue:
In the following example I pass a different generic type from the response I would get in my load function. This should throw a type error, but it doesn't, which is very frustrating, since this wont help us make the frontend more type strict.
const myDataSource = new DataSource<MyResponseDto, number>({
load: (options) => myFunction(options) //returns Promise<LoadResult<MyOtherResponseDto>>
})
Currently to avoid this issue, I have made a wrapper class for the DataSource, so I infer the types from the load
function and pass it to the DataSource, additionally I had to overwrite a few types as well, since they always have the type of any
. However I was wondering if there is a better and native solution which doesn't make me write an extra wrapper and maintain it...
We are using devextreme
& devextreme-angular
version: 24.1.7
I have posted a support ticket in the devexpress support center as well.
Current solution (wrapper):
DataSourceFactory.ts
/**
* A factory class to produce strongly typed DevExtreme data sources.
*/
export class DataSourceFactory<TLoadOptions extends Pick<CustomStoreOptions<any, any>, "load">> {
private readonly storeOptions: TLoadOptions;
constructor(storeOptions: TLoadOptions) {
this.storeOptions = storeOptions;
}
/**
* Creates an `ExtendedDataSource` by merging the given `storeOptions` and any
* additional custom store options (except `load` which is enforced in the constructor).
*/
public createDataSource(
additionalOptions?: Omit<
CustomStoreOptions<ExtractItemType<TLoadOptions>, ExtractKeyType<TLoadOptions>>,
"load" | "key"
> & { key: keyof ExtractItemType<TLoadOptions> | Array<keyof ExtractItemType<TLoadOptions>> }
): ExtendedDataSource<ExtractItemType<TLoadOptions>, ExtractKeyType<TLoadOptions>> {
const mergedOptions: CustomStoreOptions<ExtractItemType<TLoadOptions>, ExtractKeyType<TLoadOptions>> = {
...this.storeOptions,
...(additionalOptions as CustomStoreOptions),
};
return new ExtendedDataSource<ExtractItemType<TLoadOptions>, ExtractKeyType<TLoadOptions>>(mergedOptions);
}
}
ExtendedDataSource.ts:
/**
* An extension of DevExtreme's DataSource with a typed `columns()` helper.
*
* @internal
* @deprecated do not use this method. Use `DataSourceFactory` instead. This is for internal purposes only
*/
export class ExtendedDataSource<TItem, TKey = number> extends DataSource<
TItem,
TKey
> {
constructor(options: CustomStoreOptions<TItem, TKey>) {
super(options);
}
/**
* Returns a proxy-based column accessor for type-safe property paths.
*
* generic type T is required
*/
public columns = <T extends TItem = never>() => {
return getColumns<T>();
};
// The overrides below simply ensure TypeScript picks up TItem[] correctly.
public override items(): TItem[] {
return super.items();
}
public override load(): DxExtendedPromise<TItem[]> {
return super.load();
}
public override reload(): DxExtendedPromise<TItem[]> {
return super.reload();
}
}
types.ts:
/**
* @internal
* Extracts the 'item' type from an object whose `load` method returns a `Promise<LoadResult<T>>`.
*/
export type ExtractItemType<T> = T extends {
load: (options: any) => Promise<LoadResult<infer U>>;
}
? U
: unknown;
/**
* @internal
* Extracts the 'key' type from an object whose `byKey` method accepts `key`.
* Fallback to `number` if not defined.
*/
export type ExtractKeyType<T> = T extends {
byKey?: (key: infer K, extraOptions?: any) => PromiseLike<any>;
}
? K
: number;
/**
* @internal
*/
export interface StringCallable {
(): string;
}
/**
* @internal
*/
export type PropertiesOf<T> = T extends any[]
? StringCallable
: T extends object
? { [K in keyof Required<T>]-?: PropertiesOf<T[K]> } & StringCallable
: StringCallable;
I made an additional function to my wrapper to get all the possible dataField
s.
GetColumns.ts:
/**
* @internal
* @deprecated do not use this method. Use `DataSourceFactory` instead. This is for internal purposes only
*/
export function getColumns<T>(path: string = ""): PropertiesOf<T> {
return new Proxy((() => path) as StringCallable, {
get: (_target, prop: string) => {
const newPath = path ? `${path}.${prop}` : prop;
return getColumns(newPath);
},
apply: (_target, _thisArg, _args) => path,
}) as PropertiesOf<T>;
}
Example usage of wrapper:
readonly #dataSourceFactory = new DataSourceFactory({
load: (options) => myFunction(options), //returns Promise<LoadResult<MyResponseDto>>
});
readonly dataSource = this.#dataSourceFactory.createDataSource({
key: "id", //key can be keyof MyResponseDto or an array of those keys
insert: (values) => myPostFunction(values), //values has the type: MyResponseDto and return value must be Promise<MyResponseDto>
update: (key, values) =>myPutFunction(key, JSON.stringify(values))), //values has the type: MyResponseDto & key has the type: number
});
readonly columns = this.dataSource.columns<MyResponseDto>(); //if the right generic is not passed, error is thrown. The reason to pass the generic is because of the limitations in angular html template
<dx-data-grid [dataSource]="dataSource">
<dxi-column [dataField]="columns.name()" />
</dx-data-grid>