Typescript autogenerated Typings

Feature(s) impacted

Typescript autogenerated Typings

Problem

I’m encountering several difficulties with TypeScript typing in Forest Admin. I’m using auto-generated types and trying to follow documentation best practices.
Maybe adding real examples with relationships, smart actions, etc. to the documentation could help.

The code is working without typing, the field owner is an autocomplete field and work, but not when I play with TS.

Issue

  1. Basic Typing Issues
  • Auto-completion stops working with TypeScript while it works without
  • Relations work without explicit typing but break when trying to add types
  1. Specific TypeScript Errors
  • One-to-One Relations errors
  • ActionContext vs ActionContextSingle issues
  • Related Records access problems

Context

I have a classic structure with relationships:

I have “Car”, owned by “Owner”, every Owner is linked to one “Account”
Not all “Account” are “Owner”

Exemple of my collection with a relation :

const handle = (collection: CarCustomizer) => {
  collection.addAction("Edit car", {
    scope: 'Single',
    form: [
      {
        label: FIELD_OWNER
        type: 'String',
        defaultValue: async (context: ActionContextSingle) => {
          const {account_id: accountId} = await context.getRecord([
            'account_id',
          ]);
          if (!accountId) {
            return '';
          }
          const [{name}] = await context.dataSource
            .getCollection('owner')
            .list(filterOneOwner{accountId}), ['name']);
          return name || '';
        },
        isReadOnly: true,
      },

Type Structure Example

Here’s a simplified version of our auto-generated schema.ts:

export interface Schema {
  'owner': {
    plain: {
      'account_id': number | null;
      'id': number;
      'name': string | null;
    };
    nested: {
      'account': Schema['account']['plain'] & Schema['account']['nested'];
    };
    flat: {
      'account:create_time': string | null;
      'account:email': string | null;
      'account:id': number;
      'account:name': string | null;
    };
  };
  
  'account': {
    plain: {
      'create_time': string | null;
      'email': string | null;
      'id': number;
      'is_enabled': boolean | null;
      'name': string | null;
    };
    nested: {
      // Additional relations if any
    };
    flat: {
      // Flattened relations fields
    };
  };

  'car': {
    plain: {
      'id': number;
      'account_id': number | null;
      'brand': string | null;
      'year': number | null;
    };
    nested: {
      'owner': Schema['owner']['plain'] & Schema['owner']['nested'];
    };
    flat: {
      'owner:name': string | null;
      'owner:id_number': number | null;
      'owner:account:email': string | null;
      'owner:account:name': string | null;
    };
  };
}

Issues Encountered

1. One-to-One Relations

According to the documentation, I should be able to define a one-to-one relation like this:

collection.addOneToOneRelation('account', 'account', {
 originKey: 'account_id',
 originKeyTarget: 'id'
});

Car :

  associate(models: any) {
    this.Car.hasOne(models.account, {
      foreignKey: 'id',
      sourceKey: 'account_id',
    });
  }

Owner:

    this.Owner.hasOne(models.account, {
      foreignKey: 'id',
      sourceKey: 'account_id',
    });

And Account is declared.

Issues

1. One-to-One Relations

According to the documentation, I should be able to define a one-to-one relation like this:

collection.addOneToOneRelation('account', 'account', {
 originKey: 'account_id',
 originKeyTarget: 'id'
});

But I get this error:

TS2322: Type "account_id" is not assignable to type "id" | "name" | "country" | "create_time" | "email" | ...."

And besides, my relationship worked just fine before without this.

2. Actions with ActionContext vs ActionContextSingle

In my actions, I have issues with context typing:

defaultValue: async (context: ActionContext<Schema, 'car'>) => {
  const {account_id: accountId} = await context.getRecord(['account_id']);
  // ...
}

Even if I have scope: 'Single', TS throw different error

Type '(context: ActionContextSingle<Schema, "car">) => Promise<string>' 
is not assignable to type 'ValueOrHandler<ActionContext<Schema, "car">, File[]> | undefined'

If i don’t use (context: ActionContextSingle), (because ActionContext is infered and doesn’t have getRecord) I have this error.:

TS2322: Type
(context: ActionContextSingle<Schema, "car">) => Promise<string>
is not assignable to type
ValueOrHandler<ActionContext<Schema, "car">, string> | undefined

If I don’t check if I have accountID, or result for getCollection, TS give error too,

I tried a lot, like

  const ctx = context as ActionContextSingle<Schema, 'car'>;
  const {account_id: accountId} = await ctx.getRecord(['account_id']);

But even when TS compiled and I have no error, the autocomplete stop to work.

3. Auto-generated Types vs TSchema

Auto-generated types use Schema:

export type AccountCustomizer = CollectionCustomizer<Schema, 'account'>;

But it seems Forest internally uses TSchema. I tried using TSchema directly but it creates incompatibilities with auto-generated types.

4. Related Record Access Issues

When trying to access related records, type inference breaks:

const [{account: {name}}] = await ctx.dataSource
  .getCollection('owner')
  .list(filterOneOwner({accountId}), ['account:name']);
// Error: Property 'name' does not exist on type '...'

Questions

  1. What’s the correct approach to handle these typing issues?
  2. Are there any best practices for casting between ActionContext and ActionContextSingle?
  3. Do you have a complete and complex example for managing ForestAdmin with auto generated Typing and relations ?
  4. How should we handle the difference between Schema and TSchema?
  5. Relations were working before without addOneToOneRelation (thanks to Sequelize). Should we avoid explicitly adding relations?
  6. Is there a recommended way to maintain type safety without breaking existing functionality?
  7. What’s the best practice for typing complex relations in Forest Admin?

Env

  • Environment name: development
  • Agent technology: nodejs
  • Agent (forest package) name & version:
"@forestadmin/agent": "1.53.1",
"@forestadmin/datasource-sequelize": "1.10.5",
  • Database type: MySQL

Hello @Valentin_Giraud and welcome to our community,

This is strange that you should be facing issues with typescript, since the package @forestadmin/agent has been designed and built to be typescript-first, with complete typing of the customizations interface.

Some generalities that could help

  • something that I might suggest if you dont find enough or reliable examples from the doc is that you look at this package: https://@forestadmin/agent/_example
    It is part of our open source @forestadmin/agent repository and should provide a lot of examples of relationships, actions, and so one.
    This is the one that we use internally when developping/debugging, and typing is enforced and respected accross all customizations (as well as accross the whole package).
    You might also want to compare your tsconfig.json with the one in the example and see if any differences might explain your issues, especially strictNullCheck, which we recommend to set to false. Please also check that you are running a recent typescript version.

  • You have to bear in mind is that the typings files are updated everytime your datasource schema changes and your agent restarts.
    This means for instance that if you add a new column in your database, and you need to reference this column in your customizations, then you will have to restart your agent at least once for it to register this new field and add it to the typings file. After this, typescript should be accepting (and autocompleting) any references to this new field.
    Do note that this principle applies to database schema changes, but also to your customizations. Whenever you use addField or addRelations for instance, make sure you restart the agent to have them added to the typings file before working with them.

regarding your specific issues
Your post highlights many issues that might or might not be related. I will try to give pointers where I can, but please note that it is easier to manage 1 specific issue per post in our community forum:

const handle = (collection: CarCustomizer) => {
  collection.addAction("Edit car", {
    scope: 'Single',
    form: [
      {
        label: FIELD_OWNER
        type: 'String',
        defaultValue: async (context: ActionContextSingle) => {
          const {account_id: accountId} = await context.getRecord([
            'account_id',
          ]);
          if (!accountId) {
            return '';
          }
          const [{name}] = await context.dataSource
            .getCollection('owner')
            .list(filterOneOwner{accountId}), ['name']);
          return name || '';
        },
        isReadOnly: true,
      },

→ in this example your are specifying types where you probably dont need. for instance the callback for defaultValue can simply be prototyped async (context) => {
and the typescript type inference should do the rest

One-to-One Relations
→ in this example you are defining both the relation in sequelize and in forest customization. You should do only one of them. If you already have the relationship defined in sequelize, it should already be picked-up by the forest agent.

now regarding your questions:

  1. What’s the correct approach to handle these typing issues? → I hope i have given you enough details above to start fixing theme
  2. Are there any best practices for casting between ActionContext and ActionContextSingle?
    → type inference should be enough so that you dont have to cast
  3. Do you have a complete and complex example for managing ForestAdmin with auto generated Typing and relations ?
    → same here, the typings should be accurate within relations. Please look through the examples here for inspiration
  4. How should we handle the difference between Schema and TSchema?
    Schema should be all you need
  5. Relations were working before without addOneToOneRelation (thanks to Sequelize). Should we avoid explicitly adding relations?
    → yes. addOneToOneRelation are for when you dont want to touch your app’s database or sequelize models, but still want a “soft” relationship in your forest admin panel
  6. Is there a recommended way to maintain type safety without breaking existing functionality?
    → I’m not sure I understand this question. You should be able to use typescript without issues and enforcing type checking.
  7. What’s the best practice for typing complex relations in Forest Admin?
    → same here. Generated typings should work out of the box for ‘complex’ relations (by which I assume you mean deeply nested or many to many). Just something that you might want to check is the depth of the typings typingsMaxDepth options of your agent. (altough the errors that you refered to dont seem to point to an issue with this)

One last point which I forgot.
If you need even more examples or functionnality than present in the agent example I linked above, you may be interested by our plugins, which are wrapper that perform a more high level functionnality by using the basic forest agent api:

on the official repo:

some more experimental plugins that you can import or copy to adapt to your use case: