TLDR: Jump to Full Sample Coe to just see the final code.
I have a React + TypeScript + WatermelonDB project and want to:
- Create a typesafe association between related models.
- Observe a collection for changes so react components automitally re-render when items are added or removed.
I generally found WatermelonDB + TypeScript examples to be lacking, so I put together this brief example to show how I got this working.
My project uses:
- node: 20
- react: 19.0.0-rc-02c0e824-20241028
- @nozbe/watermelondb: ^0.27.1
- typescript: ^5
First, here are the schema and models without the relationships. I will show how the relationships get added below.
https://watermelondb.dev/docs/Schema
// ./schema.ts
import { appSchema, tableSchema } from '@nozbe/watermelondb'
export const schema = appSchema({
version: 1,
tables: [
tableSchema({
name: 'users',
columns: [
{ name: 'name', type: 'string' },
{ name: 'email', type: 'string' },
{ name: 'created_at', type: 'number' },
{ name: 'updated_at', type: 'number' },
],
}),
tableSchema({
name: 'appointments',
columns: [
{ name: 'scheduled', type: 'number' },
{ name: 'status', type: 'string' },
{ name: 'created_at', type: 'number' },
{ name: 'updated_at', type: 'number' },
],
}),
],
})
https://watermelondb.dev/docs/Model
// ./User.ts
import { Model, Query } from '@nozbe/watermelondb'
import { readonly, text, date } from '@nozbe/watermelondb/decorators'
export default class User extends Model {
static table = 'users'
@text('name') name!: string
@text('email') email!: string
@readonly @date('created_at') createdAt!: number
@readonly @date('updated_at') updatedAt!: number
}
// ./Appointment.ts
import { Model } from '@nozbe/watermelondb'
import { readonly, text, date } from '@nozbe/watermelondb/decorators'
export default class Appointment extends Model {
static table = 'appointments'
@date('scheduled') scheduled!: number
@text('status') status!: string
@readonly @date('created_at') createdAt!: number
@readonly @date('updated_at') updatedAt!: number
}
https://watermelondb.dev/docs/Setup
Don’t forget to add your model classes to the adapter.
// ./adapter.ts
...
const database = new Database({
adapter,
+ modelClasses: [Appointment, User]
})
...
https://watermelondb.dev/docs/Relation
Now we begin the work of associating the appointments with their user. While this isn’t overly complicated, I find this isn’t a well documented workflow when using Typescript, so it took a while to work out this exact syntax. Hopefully you find this example helpful!
Let’s update the schema to add the relationship associating an appointment with a user.
tableSchema({
name: 'appointments',
columns: [
+ { name: 'user_id', type: 'string', isIndexed: true },
...
],
}),
Now let’s update the models so they know about the relationship.
Notice the appointments
field is typed as a Query
.
import { Model, Query } from '@nozbe/watermelondb'
- import { readonly, text, date } from '@nozbe/watermelondb/decorators'
+ import { readonly, text, date, children } from '@nozbe/watermelondb/decorators'
+ import Appointment from './Appointment'
export default class User extends Model {
static table = 'users'
+ static associations = {
+ appointments: { type: 'has_many', foreignKey: 'user_id' },
+ } as const
+
+ @children('appointments') appointments!: Query<Appointment>
@text('name') name!: string
@text('email') email!: string
@readonly @date('created_at') createdAt!: number
@readonly @date('updated_at') updatedAt!: number
}
The type Query<Appointment>
is important to enable user.appointments.query()
for fetching the associated collection of Appointment[]
.
Notice:
- The
user
relationship is typed as aRelationship
. - The
!
tells us thatuser
should always be defined. It is up to you to ensure you set this when creating a new appointment instance.
import { Model } from '@nozbe/watermelondb'
- import { readonly, text, date } from '@nozbe/watermelondb/decorators'
+ import { readonly, text, date, relation } from '@nozbe/watermelondb/decorators'
+ import User from './User'
+ import { Relation } from '@nozbe/watermelondb'
export default class Appointment extends Model {
static table = 'appointments'
+ static associations = {
+ appointments: { type: 'belongs_to', key: 'user_id' },
+ } as const
+
+ @relation('users', 'user_id') user!: Relation<User>
@date('scheduled') scheduled!: number
@text('status') status!: string
@readonly @date('created_at') createdAt!: number
@readonly @date('updated_at') updatedAt!: number
}
Now, when you have fetched a user, you can query the associated collections like this.
const appointments = await user.appointments.fetch()
And the appointments
object can be used with enhanced reactive components.
TODO: demonstrate using the relationship in an enhanced list component.
// ./schema.ts
import { appSchema, tableSchema } from '@nozbe/watermelondb'
export const schema = appSchema({
version: 1,
tables: [
tableSchema({
name: 'users',
columns: [
{ name: 'name', type: 'string' },
{ name: 'email', type: 'string' },
{ name: 'created_at', type: 'number' },
{ name: 'updated_at', type: 'number' },
],
}),
tableSchema({
name: 'appointments',
columns: [
{ name: 'user_id', type: 'string', isIndexed: true },
{ name: 'scheduled', type: 'number' },
{ name: 'status', type: 'string' },
{ name: 'created_at', type: 'number' },
{ name: 'updated_at', type: 'number' },
],
}),
],
})
// ./User.ts
import { Model, Query } from '@nozbe/watermelondb'
import { readonly, text, date, children } from '@nozbe/watermelondb/decorators'
import Appointment from './Appointment'
export default class User extends Model {
static table = 'users'
static associations = {
appointments: { type: 'has_many', foreignKey: 'user_id' },
} as const
@children('appointments') appointments!: Query<Appointment>
@text('name') name!: string
@text('email') email!: string
@readonly @date('created_at') createdAt!: number
@readonly @date('updated_at') updatedAt!: number
}
// ./Appointment.ts
import { Model } from '@nozbe/watermelondb'
import { readonly, text, date, relation } from '@nozbe/watermelondb/decorators'
import User from './User'
import { Relation } from '@nozbe/watermelondb'
export default class Appointment extends Model {
static table = 'appointments'
static associations = {
appointments: { type: 'belongs_to', key: 'user_id' },
} as const
@relation('users', 'user_id') user!: Relation<User>
@date('scheduled') scheduled!: number
@text('status') status!: string
@readonly @date('created_at') createdAt!: number
@readonly @date('updated_at') updatedAt!: number
}