Nozbe / WatermelonDB

🍉 Reactive & asynchronous database for powerful React and React Native apps ⚡️
https://watermelondb.dev
MIT License
10.44k stars 586 forks source link

How to type nullable relations? #1801

Open itsramiel opened 3 months ago

itsramiel commented 3 months ago

Hey,

I have a use case where a relationship can be nullable. I defined this in the schema using the isOptional but how would I go about typing the field in the model?

Given the models in the example of this repo:

class Blog extends Model {
  static table = TableName.BLOGS

  static associations: Associations = {
    [TableName.POSTS]: { type: 'has_many', foreignKey: 'blog_id' },
  }

  @field('name') name!: string;

}

class Post extends Model {
  static table = TableName.POSTS

  static associations: Associations = {
    [TableName.BLOGS]: { type: 'belongs_to', key: 'blog_id' },
  }

  @field('name') name!: string;
  @text("body") content!: string;

  @relation(TableName.BLOGS, 'blog_id') blog!: Relation<Blog>;
}

Let's assume for the sake of example that you post a Post under no specific Blog how would: @relation(TableName.BLOGS, 'blog_id') blog!: Relation<Blog>; change?

Changing the type to Relation<Blog> | null doesnt work correctly because post.blog always return an object even the blog is nullable. And Relation<Blog | null> doesnt work because the generic that Relation takes extends Model which is not nullable.

How do I go about creating nullable relations with type safety?

nhanders commented 2 months ago

Hi @itsramiel , I had the same issue here and this is how I resolved it. I don't love that I'm ignoring TS errors and using a type assertion, but it does the job for me.

import {
  Model, Relation as WatermelonDbRelation,
} from '@nozbe/watermelondb';
import {
  Observable,
} from 'rxjs';

/**
 * Wrapper of `Relation` to correctly type optional relations. These are
 * relations where the fk is an optional field.
 */
export default class OptionalRelation<
  T extends Model,
> extends WatermelonDbRelation<T> {
  // @ts-ignore
  fetch() {
    return super.fetch() as Promise<T | null>;
  }

  // @ts-ignore
  observe() {
    return super.observe() as Observable<T | null>;
  }
}
@relation(TableName.BLOGS, 'blog_id') blog!: OptionalRelation<Blog>;