jakearchibald / idb

IndexedDB, but with promises
https://www.npmjs.com/package/idb
ISC License
6.2k stars 348 forks source link

Typescript: cannot compose schema from multiple files #289

Open okomarov opened 1 year ago

okomarov commented 1 year ago

This simplified example can be found on Replit.

I want to organise my file structure by store:

index.ts
  user.ts
  portfolio.ts
  ...

In index.ts I have:

import { openDB } from 'idb';

import { PortfolioSchema, createPortfolio } from './portfolio';
import { UserSchema, createUser } from './user';

interface Schema extends PortfolioSchema, UserSchema {}

const main = async () => {
  const client = await openDB<Schema>('myDb', 1, {
    upgrade(db) {
      createPortfolio(db);
      createUser(db);
    },
  });
};

where the first issue with e.g. createPortfolio(db):

Argument of type 'IDBPDatabase' is not assignable to parameter of type 'IDBPDatabase'. Types of property 'objectStoreNames' are incompatible. Type 'TypedDOMStringList<"portfolio" | "user">' is not assignable to type 'TypedDOMStringList<"portfolio">'. Type '"portfolio" | "user"' is not assignable to type '"portfolio"'. Type '"user"' is not assignable to type '"portfolio"'.

which is of the type A | B is not assignable to A. So, I want to narrow down db, where the definition of the sub-schema in portfolio.ts and the type guard are (the user.ts is very similar):

import { DBSchema, IDBPDatabase } from 'idb';

interface Portfolio {
  userId: string;
  portfolioId: string;
}

export interface PortfolioSchema extends DBSchema {
  portfolio: {
    key: string[];
    value: Portfolio;
  };
}

export const createPortfolio = (db: IDBPDatabase<PortfolioSchema>) => {
  db.createObjectStore('portfolio', { keyPath: ['userId', 'teamId', 'portfolioId'] });
};

export const isPortfolio = <T extends PortfolioSchema>(db: IDBPDatabase<T>): db is IDBPDatabase<PortfolioSchema> =>
  'portfolio' in db.objectStoreNames;

The issue with the type guard is:

A type predicate's type must be assignable to its parameter's type. Type 'IDBPDatabase' is not assignable to type 'IDBPDatabase'. Types of property 'objectStoreNames' are incompatible. Type 'TypedDOMStringList<"portfolio">' is not assignable to type 'TypedDOMStringList<StoreNames>'. Type '"portfolio"' is not assignable to type 'StoreNames'.

How can I achieve sub-schema composition where interfaces, sub-schemas and methods are all defined in separate files?

keitwb commented 7 months ago

I had the same issue. As lame as this is, I ended up just using the as operator to cast the DB when calling the create* methods in the root db upgrade function like so:

const main = async () => {
  const client = await openDB<Schema>('myDb', 1, {
    upgrade(db) {
      createPortfolio(db as unknown as PortfolioSchema);
      createUser(db as unknown as UserSchema);
    },
  });
};

Very much a hacky workaround but I fiddled for a while with typings to make it work and couldn't find any trick to it. Given that the DBSchema uses index types I suspect it isn't possible, but would be interested if any Typescript experts know of any way.

I'm not sure about the type guard issue.