React-admin: Dumb dataProvider doesn't type check

Created on 3 Nov 2020  路  4Comments  路  Source: marmelab/react-admin

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
TypeScript bug help wanted

Most helpful comment

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;

All 4 comments

Thanks for reporting. Reproduced

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;

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.

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)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kopax picture kopax  路  3Comments

marknelissen picture marknelissen  路  3Comments

samanmohamadi picture samanmohamadi  路  3Comments

Dragomir-Ivanov picture Dragomir-Ivanov  路  3Comments

ilaif picture ilaif  路  3Comments