Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dumb dataProvider doesn't type check #5476

Closed
DjebbZ opened this issue Nov 3, 2020 · 14 comments · Fixed by #6634
Closed

Dumb dataProvider doesn't type check #5476

DjebbZ opened this issue Nov 3, 2020 · 14 comments · Fixed by #6634

Comments

@DjebbZ
Copy link
Contributor

DjebbZ commented Nov 3, 2020

What you were expecting:
Bootstrap a dummy application with create-react-app with typescript, and implement a dummy dataProvider. It should compile just fine.

What happened instead:
Big Typescript error :

Type '{ getList: (resource: string, params: any) => Promise<{ data: { id: number; toto: string; }[]; total: number; }>; getOne: (resource: string, params: any) => Promise<{ data: { id: number; toto: string; }; }>; ... 6 more ...; deleteMany: (resource: string, params: any) => Promise<...>; }' is not assignable to type 'DataProvider | LegacyDataProvider'.
  Type '{ getList: (resource: string, params: any) => Promise<{ data: { id: number; toto: string; }[]; total: number; }>; getOne: (resource: string, params: any) => Promise<{ data: { id: number; toto: string; }; }>; ... 6 more ...; deleteMany: (resource: string, params: any) => Promise<...>; }' is not assignable to type 'DataProvider'.
    The types returned by 'getList(...)' are incompatible between these types.
      Type 'Promise<{ data: { id: number; toto: string; }[]; total: number; }>' is not assignable to type 'Promise<GetListResult<RecordType>>'.
        Type '{ data: { id: number; toto: string; }[]; total: number; }' is not assignable to type 'GetListResult<RecordType>'.
          Types of property 'data' are incompatible.
            Type '{ id: number; toto: string; }[]' is not assignable to type 'RecordType[]'.
              Type '{ id: number; toto: string; }' is not assignable to type 'RecordType'.
                '{ id: number; toto: string; }' is assignable to the constraint of type 'RecordType', but 'RecordType' could be instantiated with a different subtype of constraint 'Record'.  TS2322

    65 | const App: React.FC = () => {
    66 |     return (
  > 67 |         <Admin loginPage={LoginPage} dataProvider={dataProvider}/>
       |                                      ^
    68 |     );
    69 | };
    70 |

Steps to reproduce:

  1. npx create-react-app my-app --template typescript
  2. cd my_app
  3. npm install react-admin
  4. npm start

Related code:
In App.tsx:

import React from 'react';
import {Admin} from "react-admin";

const dataProvider = {
  getList: (resource: string, params: any) => Promise.resolve({data: [{id: 1, toto: "tata"},{id: 2, toto: "tata"}], total: 0}),
  getOne: (resource: string, params: any) => Promise.resolve({data: {id: 1, toto: "tata"}}),
  getMany: (resource: string, params: any) => Promise.resolve({data: [{id: 1, toto: "tata"}]}),
  getManyReference: (resource: string, params: any) => Promise.resolve({data: [{id: 1, toto: "tata"}]}),
  create: (resource: string, params: any) => Promise.resolve({data: {id: 1, toto: "tata"}}),
  update: (resource: string, params: any) => Promise.resolve({data: {id: 1, toto: "tata"}}),
  updateMany: (resource: string, params: any) => Promise.resolve({data: [1]}),
  delete: (resource: string, params: any) => Promise.resolve({data: {id: 1, toto: "tata"}}),
  deleteMany: (resource: string, params: any) => Promise.resolve({data: [1]}),
};

function App() {
  return (
      <Admin dataProvider={dataProvider}/>
  );
}

export default App;

Other information:

Environment

  • React-admin version: 3.9.6
  • Last version that did not exhibit the issue (if applicable):
  • React version: ^17.0.1
  • Browser: Chrome
@djhi
Copy link
Collaborator

djhi commented Nov 3, 2020

Thanks for reporting. Reproduced

@fzaninotto
Copy link
Member

In the meantime, you can make it compile with

import React from 'react';
-import {Admin} from "react-admin";
+import { Admin, DataProvider } from "react-admin";

const dataProvider = {
  getList: (resource: string, params: any) => Promise.resolve({data: [{id: 1, toto: "tata"},{id: 2, toto: "tata"}], total: 0}),
  getOne: (resource: string, params: any) => Promise.resolve({data: {id: 1, toto: "tata"}}),
  getMany: (resource: string, params: any) => Promise.resolve({data: [{id: 1, toto: "tata"}]}),
  getManyReference: (resource: string, params: any) => Promise.resolve({data: [{id: 1, toto: "tata"}]}),
  create: (resource: string, params: any) => Promise.resolve({data: {id: 1, toto: "tata"}}),
  update: (resource: string, params: any) => Promise.resolve({data: {id: 1, toto: "tata"}}),
  updateMany: (resource: string, params: any) => Promise.resolve({data: [1]}),
  delete: (resource: string, params: any) => Promise.resolve({data: {id: 1, toto: "tata"}}),
  deleteMany: (resource: string, params: any) => Promise.resolve({data: [1]}),
-};
+} as DataProvider;

@DjebbZ
Copy link
Contributor Author

DjebbZ commented Nov 3, 2020

Thanks, I confirm this "trick" works. This unblocks me at least. Leaving the issue opened since there seems to be a deeper problem.

Note that the notation const dataProvider: DataProvier = { doesn't work either, same problem.

@bayareacoder
Copy link

bayareacoder commented Nov 7, 2020

We struggled with the same and fixed as follows, to have typing of a data provider that includes custom methods:

  1. Define your own DataProvider class that implements all your methods, and with a method, in our case called v3(), that returns a data provider in react-admin v3 format.
    Make sure to fix this in the constructor.

class DataProvider {
  constructor(options: NDataProvider.IConstructorOptions) {
    [
<your list of data provider methods>
    ].forEach((fnName) => {
      (this as any)[fnName] = (this as any)[fnName].bind(this);
    });
}
  /**
   * @returns the v3 data provider object
   */
  v3() {
    return {
      ...pick(this, [
        'create',
        'delete',
        'deleteMany',
        'getList',
        'getMany',
        'getManyReference',
        'getOne',
        'update',
        'updateMany',
      ...<your extra custom methods>,
      ]),
    };
  }
}
// implement your methods, with ra typing or stricter typing
  @Silly(FILE)
  @CatchAllReject('Cannot create')
  create<RecordType extends RaRecord = RaRecord>(
    resource: EnumResource,
    params: CreateParams,
  ): Promise<CreateResult<RecordType>> {
    //  ...
  }
  1. Create a class instance on startup, store it in local state, and set the data provider on react-admin:
const [dataProvider,setDataProvider]= useState<any>(null);
useEffect(()=> {
      const dataProvider = new DataProvider({
          // any init options go here
      });
     setDataProvider(dataProvider);
},[]);

// include a `return <Loading/>` here  since RA requires a valid data provider so you need to bail ahead of below if it's not yet initialized

<Admin dataProvider={dataProvider.v3()} > ...

  1. Now anywhere you use the data provider you can have your own typing as follows. Lodash's pick will preserve the types of each of your methods...nice.
export type TDataProvider = ReturnType<DataProvider['v3']>;

Caveat: this may not be 100% correct to cast what returns from useDataProvider to TDataProvider, since it doesn't include the Proxy, but more important for us to make sure we can check that input to, and response from, data provider methods is correctly typed everywhere)

The main reason we use a class is since our data provider is composed of different "sub-data providers" (one for each backend API) . Basically it's an abstract class where we can do things for initialization of each sub-data provider. Another benefit is that you can use decorators on class methods (eg as in above, decorators for logging and for returning a rejected promise from a thrown error, as required by react-admin)

@harishy100
Copy link

harishy100 commented May 12, 2021

It's been 7 months since this was opened. Any possibility of a fix sometime soon? This is one of the most basic use cases.

@fzaninotto
Copy link
Member

@harishy100 This is an open-source project. Anyone can contribute. It's not a critical problem for the core team, so we dedicate time to other problems for now.

If you want the bug fixed quickly, the best way is to fork the project, fix it yourself, and open a PR to have your fix merged in the react-admin code. If you don't have the time or skills to do so, consider hiring Marmelab to fix the bug for you.

@tmarcinkowski-logitech
Copy link

Just adding my two cents as I've also stumbled upon this in my first project with RA.
Isn't it just because of that rigid assignment to Record at every method of DataProvider? Like in this example:

getList: <RecordType extends Record = Record>(resource: string, params: GetListParams) => Promise<GetListResult<RecordType>>;

At least that's how I understood that longy explanation: https://stackoverflow.com/a/59363875

@coldshine
Copy link

Had the similar issue on 4.0.2 with getOne:

src/components/App.tsx:13:47 - error TS2322: Type '{ create: (resource: any, { data: formData }: CreateParams) => Promise<{ data: any; }>; delete: (resource: any, { id }: DeleteParams) => Promise<DeleteResult>; deleteMany: () => Promise<DeleteManyResult>; getList: (resource: any, params: GetListParams) => Promise<{ data: any[]; total: number; }>; ... 4 more ...; upd...' is not assignable to type 'DataProvider<string> | LegacyDataProvider'.
  Type '{ create: (resource: any, { data: formData }: CreateParams) => Promise<{ data: any; }>; delete: (resource: any, { id }: DeleteParams) => Promise<DeleteResult>; deleteMany: () => Promise<DeleteManyResult>; getList: (resource: any, params: GetListParams) => Promise<{ data: any[]; total: number; }>; ... 4 more ...; upd...' is not assignable to type 'DataProvider<string>'.
    The types returned by 'getOne(...)' are incompatible between these types.
      Type 'Promise<{ data: { id: any; }; }>' is not assignable to type 'Promise<GetOneResult<RecordType>>'.
        Type '{ data: { id: any; }; }' is not assignable to type 'GetOneResult<RecordType>'.
          Types of property 'data' are incompatible.
            Type '{ id: any; }' is not assignable to type 'RecordType'.
              '{ id: any; }' is assignable to the constraint of type 'RecordType', but 'RecordType' could be instantiated with a different subtype of constraint 'RaRecord'.

13   <Admin authProvider={authProvider} dataProvider={dataProvider} disableTelemetry>
                                             ~~~~~~~~~~~

And my getOne:

import { GetOneParams } from 'react-admin';

// ... code

getOne: async (resource: any, { id }: GetOneParams) => {
    return { data: { id } };
},

I think the key here is The types returned by 'getOne(...)' are incompatible

Setting proper return type GetOneResult has fixed the issue:

import { GetOneParams, GetOneResult } from 'react-admin';

// ... code

getOne: async (resource: any, { id }: GetOneParams): Promise<GetOneResult> => {
    return { data: { id } };
}

@maze-le
Copy link

maze-le commented Jun 24, 2022

The type GetOneResult is generic and cannot be instantiated with any concrete type (except any). Most people stumbling upon this would want to use a concrete GetOneResult return type like for example:

interface PoiDTO extends RaRecord { id: number; geom: PointGeometry; [key: string]: any; };

async getOne(resource: string, params: GetOneParams): Promise<GetOneResult<PoiDTO>>

Which is pretty much impossible. I am using the generic defaults (any) now, like @coldshine mentioned. It would be really nice to have this mentioned in the documentation somewhere... The generic type parameter for GetOneResult creates the wrong expectation to provide a concrete type for RecordType.

@fzaninotto
Copy link
Member

@maze-le I don't understand your point. If you call dataProvider.getOne<PoiDTO>(), TS will tell you that the return type is Promise({ data: PoiDTO }). What did you expect?

@maze-le
Copy link

maze-le commented Jun 24, 2022

@fzaninotto: Calling it like this works, you are right. But I have a slightly different situation: We basically have 2 backend services -- one for read-operations based on elasticsearch and one for write-operations that operates directly on the Postgres database.

For this I have started to implement a custom data provider -- and this works well, except when it comes to the generic type restrictions:

const poiProvider: DataProvider = {
  async getOne(resource: string, params: GetOneParams): Promise<GetOneResult> {
    const resourceId = params.id;
    const url = `${apiBaseUrl}/${resource}/${resourceId}`;
    const httpReturn: HttpReturn<PoiDTO> = await httpClient.fetch<PoiDTO>(url);

    return { data: httpReturn.json };
  };
  
  // ... and so on...
}

This works well. But what I wanted to do is to restrict the return type of the getOne() method to types of the type PoiDTO (which extends from RaRecord, so subtyping should be possible) like this:

const poiProvider: DataProvider = {
  async getOne(resource: string, params: GetOneParams): Promise<GetOneResult<PoiDTO>> {
    // ...
  };
  
  // ... and so on...
}

This fails, because the 'RecordType' could be instantiated with a different subtype of constraint 'RaRecord'.

@fzaninotto
Copy link
Member

Right, gotcha. If you have an idea of how to solve this, please open a PR!

@qisaw
Copy link

qisaw commented May 14, 2024

Can we reopen this issue?

The fact that you can't do this very simple typing is kind of a problem, especially if you do anything sophisticated with splitting up data providers.

@fzaninotto
Copy link
Member

We'd love this issue to be solved, too, but we've already spent quite some time on it without finding a solution. This is somehow a TypeScript limitation. Besides, there is a working workaround.

Reopening the issue won't solve it. We welcome any PR providing a solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants