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:
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();
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?
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
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.
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
.