最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - Type strict DataSources - Stack Overflow

programmeradmin1浏览0评论

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 dataFields. 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>
发布评论

评论列表(0)

  1. 暂无评论