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.
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.