typescriptindexeddbdexie

Dexie doesn't return Class type on query, when using mapToClass


I am experimenting with using Dexie for a project, and I am trying to use both Typescript and the .mapToClass functionality in tandem. When trying to implement the example at Dexie: Typescript, I am getting some mixed results. Here's an example of what I mean:

The setup

models/Entry.ts

export default abstract class Entry<T extends {}> {
  constructor(props: T, hiddenPropKeys?: string[]) {
    // merge props with instance to initialize all props from the interface/class
    Object.assign(this, props);

    // programmatically adding non-serializable keys
    hiddenPropKeys?.forEach((key) =>
      Object.defineProperty(this, key, { writable: true }),
    );
  }
}

models/Friend.ts

import db from '../db';
import Entry from './Entry';

interface IFriend {
  id?: number;
  name: string;
  relationship: string;
}

const table = db.friends;
type Type = IFriend; // shorthand, mainly for the static method

// doing this instead of having to iterate over every interface member again, as part of the class 
interface Friend extends Type {
  addresses: string[]
}
// eslint-disable-next-line no-redeclare
class Friend extends Entry<Type> {
  constructor(params: Type) {
    super(params, ['addresses']);
  }

  save() {
    /* do some stuff to process the Friend */
    return table.put(this);
  }

  getAddresses() {
    /* some method to grab addresses and store them locally */
    this.addresses = ['123 Main St', '55 Foo Bar Ln'];
  }

  // static helper basically do a db lookup
  // I was thinking to add a caching layer here as well
  // it won't stop you from attempting to search by non-indexed fields, but neither will Dexie
  static find<T extends keyof Type>(
    key: T,
    value: Required<Type>[T] | Required<Type>[T][],
  ) {
    const query = table.where(key);
    return value instanceof Array ? query.anyOf(value) : query.equals(value);
  }
}

// the Dexie sugar
table.mapToClass(Friend);

export default Friend;

db.ts

import Dexie, { Table } from 'dexie';
import { IFriend } from './models/Friend';

export class DataBase extends Dexie {
  friends!: Table<IFriend, number>;

  constructor() {
    super('Projector');
    this.version(1).stores({
      friends: '++id, &name'
    });
  }
}

const db = new DataBase();

The problem

Main.ts

...
const friend = await Friend.find('name', 'Frank').toArray();
console.log(friend instanceof Friend); 
// true <<< good

friend.getAddresses(); 
// TS: Property 'getAddresses' does not exist on type 'IFriend' <<< the issue
...

For some reason, the results from toArray() are coming back as advertised via mapToClass(), but not typed correctly to be used with TypeScript. Is there any way to fix this?

Things I've tried

I tried to write a custom addon, but I don't think this does anything useful.

db.Table.prototype.toArray = Dexie.override(
  db.Table.prototype.toArray,
  function (original) {
    return function () {
      return original
        .apply(this, arguments)

        // this would be dynamic, ideally, but i was just trying to get something to happen
        .map((entry) => entry as Friend); 
    };
  },
);

I can also just manually do something like this:

const friends = await Friend.find('name', ['Fred', 'Bob']).toArray(f => f as Friend[]);

but that kind-of defeats the purpose.

I've also tried switching up the Table type to use Friend instead

...
export class DataBase extends Dexie {
  friends!: Table<Friend, number>; // Class instead of interface

  constructor() {
    super('Projector');
    this.version(1).stores({
      friends: '++id, &name'
    });
  }
}
...

but that just throws errors when adding new rows, as the methods seem to get enumerated into the type, for some reason.

Thanks in advance, for any help

Update:

If I change the table definition to use the class instead of the object interface:

export class DataBase extends Dexie {
  friends!: Table<Friend, Friend['id']>; // <<< using class instead

  constructor() {
    super('Projector');
    this.version(1).stores({
      friends: '++id, &name'
    });
  }
}

I lose the ability to table.add() with an IFriend object (which is kind-of ok), but it seems to always return the correct type. When I try to add via object after this change, it complains about the instance methods missing.

Update2:

using EntityTable<Friend, 'id'> instead of Table<Friend, number> to define the data set fixes the issues from the first update.


Solution

  • This issue is addressed in dexie 4. See https://dexie.org/roadmap/dexie4.0#distinguish-insert--from-output-types

    Install dexie@4 using npm install dexie@next.