jakearchibald / idb

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

Support discriminated union type or similar in database schemas #275

Open darrylnoakes opened 2 years ago

darrylnoakes commented 2 years ago

Currently, there is no way to strongly type a keyval store that has only certain keys (each with its own value type).

For example:

interface DatabaseSchema extends DBSchema {
  metadata:
    | {
        key: "foo";
        value: { bar: number };
      }
    | {
        key: "baz";
        value: string;
      };
}

...

const foo = await db.get("metadata", "foo");
// Type of `foo`: string | { bar: number } | undefined

What I would like is some way for a discriminated union (based on key type, obviously) to be detected and used. The key passed to database functions would then be used to provide better typing.

One of my other ideas for what this could look like, instead of a discriminated union:

interface Metadata extends DBSchemaStore = {
  foo: { bar: number };
  baz: string;
};

interface DatabaseSchema extends DBSchema {
  metadata: Metadata;
}

const foo = await db.get("metadata", "foo");
// Type of `foo`: { bar: number } | undefined
darrylnoakes commented 2 years ago

Or even something like the following, where the corresponding type for every key is indexable, with the top-level entries of said type being used as the key/value pairs.

Contrived example:

interface DatabaseSchema extends DBSchema {
  posts: Record<string, Post>; // Like `list: { key: string; value: Post }`
  metadata: {
    times: { joined: string, lastSeen: string };
    counts: { posts: number; privateMessages: number }; 
  };
}
darrylnoakes commented 2 years ago

Relevant: jcalz's answer to "Generic Functions for Various Data Types in TypeScript" on StackOverflow

indianakernick commented 1 year ago

This seems doable. A separate generic parameter would need to be introduced for the key. That key can then be used to narrow the type of the value. Here's an updated version of the StoreValue type and the get function. If the Key parameter on the StoreValue type has a default value of any, then this isn't a breaking change.

export type StoreValue<
  DBTypes extends DBSchema | unknown,
  StoreName extends StoreNames<DBTypes>,
  Key = any
> = DBTypes extends DBSchema
    ? (DBTypes[StoreName] & (Key extends StoreKey<DBTypes, StoreName> ? { key: Key } : {}))['value'] 
    : any;

get<
  Name extends StoreNames<DBTypes>,
  Key extends StoreKey<DBTypes, Name> | IDBKeyRange
>(
  storeName: Name,
  query: Key
): Promise<StoreValue<DBTypes, Name, Key> | undefined>;

This works on the initial example. When anIDBKeyRange is used as the key, the value is the full union of types.