question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Please review my (potentially) silly TypeScript

See original GitHub issue

This library takes the IndexedDB API, adds a couple of properties, and changes the return types to use promises rather than IDBRequests.

The 4.0.0 branch hasn’t shipped yet, so any of this can be completely changed following feedback.

Feel free to dive in, but I’ll try to cover the individual parts. Even reviewing a small part of this will be valuable to me.

Typing proxies

The implementation is pretty small. It creates a proxy around existing IDB types, and hijacks getters to transform the returned types. The majority of the source is type definitions.

I’ve created phoney types to represent the wrapped objects, so I can document the stuff I’ve added/altered while inheriting the rest from the original type. Is this reasonable? I’m not sure how else to type proxies.

I’ve had to hack around a bit so TypeScript will let me overwrite particular methods & properties. Is this the best way of doing it?

Typing a database

My aim is to allow developers to provide optional types for a database when the database is opened, and the correct types are used throughout the database.

If any of this doesn’t make sense, or is doing something the long way around, please let me know. I felt like I had to fight TypeScript a lot, so I wouldn’t be surprised if I’ve ended up in a bad place.

Structure of IndexedDB

An origin can have many databases, and each database can have many object stores.

  • An object store has:
    • A name, which is a string, unique to this database.
    • Records, each of which has:
      • A value, which is any type (in TS terms), but I’d like developers to be able to specify a type for all records within a store.
      • A key, which is an IDBValidKey, but I’d like developers to be able to specify a more specific type for all records within a store.
    • Indexes, each of which has:
      • A name, which is a string, unique to this object store.
      • Records, each of which has:
        • A value which is the same type as the value of the store’s records.
        • A key which is an IDBValidKey, but I’d like developers to be able to specify a more specific type for all records within an index.

Defining a schema

A single interface provides the ‘schema’ of the database:

interface MyDBSchema extends DBSchema {
  'key-val-store': {
    key: string,
    value: number,
  };
  'object-store': {
    value: {
      id: number,
      title: string,
      date: Date,
    },
    key: number,
    indexes: { 'by-date': Date },
  };
}

This defines a database with two stores, ‘key-val-store’ and ‘object-store’. In key-val-store’s records, the key is a string, and the value is a number. In object-store’s records, the key is a number, and the value is an object with typed properties. object-store also has an index named ‘by-date’ which has a key of type Date.

The developer provides this when opening the database:

const db = await openDb<MyDBSchema>(…args);

Here are my types for DBSchema:

interface DBSchema {
  [s: string]: DBSchemaValue;
}

interface DBSchemaValue {
  key: IDBValidKey;
  value: any;
  indexes?: IndexKeys;
}

interface IndexKeys {
  [s: string]: IDBValidKey;
}

My aim here is to ensure the keys remain valid keys. Is this a good way to do this?

Now, my IDBPDatabase type takes the schema as an optional generic:

interface IDBPDatabase<
  DBTypes extends DBSchema | void = void,
> extends IDBPDatabaseExtends {
  // …
}

If a schema isn’t provided, the types should fall back to basic IDB types.

Type helpers

Throughout the type definitions, I need to query the schema for something specific. For instance, in db.transaction(store), store is the name of one of the object stores.

type KnownKeys<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never;

type StoreNames<DBTypes extends DBSchema | void> =
  DBTypes extends DBSchema ? KnownKeys<DBTypes> : string;

So StoreNames<MyDBSchema> will be 'key-val-store' | 'object-store'. I use KnownKeys to undo the indexing defined by DBSchema. I didn’t write KnownKeys, it kinda terrifies me, but it seems to work.

If a schema isn’t provided, the type falls back to string.

I ended up using void rather than undefined. It seems that undefined does extend DBSchema, but void doesn’t??

Store properties

type StoreValue<DBTypes extends DBSchema | void, StoreName extends StoreNames<DBTypes>> =
  DBTypes extends DBSchema ? DBTypes[StoreName]['value'] : any;

type StoreKey<DBTypes extends DBSchema | void, StoreName extends StoreNames<DBTypes>> =
  DBTypes extends DBSchema ? DBTypes[StoreName]['key'] : IDBValidKey;

type IndexNames<DBTypes extends DBSchema | void, StoreName extends StoreNames<DBTypes>> =
  DBTypes extends DBSchema ? keyof DBTypes[StoreName]['indexes'] : string;

These let me get the value type, key type, and index names for a named store.

This means my interfaces can take the schema, along with anything they need to know to identify their part within the schema, and the type helpers can do the rest:

export interface IDBPObjectStore<
  DBTypes extends DBSchema | void = void,
  StoreName extends StoreNames<DBTypes> = StoreNames<DBTypes>,
> extends IDBPObjectStoreExtends {
  get(query: StoreKey<DBTypes, StoreName> | IDBKeyRange):
    Promise<StoreValue<DBTypes, StoreName> | undefined>;
  //…
}

Index key

type IndexKey<
  DBTypes extends DBSchema | void,
  StoreName extends StoreNames<DBTypes>,
  IndexName extends IndexNames<DBTypes, StoreName>,
> = DBTypes extends DBSchema ? IndexName extends keyof DBTypes[StoreName]['indexes'] ?
  DBTypes[StoreName]['indexes'][IndexName] : IDBValidKey : IDBValidKey;

This lets me get the key for a particular index. I didn’t think I’d need the IndexName extends keyof DBTypes[StoreName]['indexes'] part, but without it it won’t let me use IndexName as an index.

Cursor source

You can cursor over a store or an index. Cursors have a source property which points to the store or index that created them.

type CursorSource<
  DBTypes extends DBSchema | void,
  StoreName extends StoreNames<DBTypes>,
  IndexName extends IndexNames<DBTypes, StoreName> | void,
> = IndexName extends IndexNames<DBTypes, StoreName> ?
  IDBPIndex<DBTypes, StoreName, IndexName> :
  IDBPObjectStore<DBTypes, StoreName>;

The idea here is IndexName is optional. If it’s provided, it assumes the source is an index, otherwise it’s an object store. This means I can do:

export interface IDBPCursor<
  DBTypes extends DBSchema | void = void,
  StoreName extends StoreNames<DBTypes> = StoreNames<DBTypes>,
  IndexName extends IndexNames<DBTypes, StoreName> | void = void,
> extends IDBPCursorExtends {
  readonly source: CursorSource<DBTypes, StoreName, IndexName>;
}

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:7 (6 by maintainers)

github_iconTop GitHub Comments

4reactions
NotWoodscommented, Feb 17, 2019

DBSchema is a little unusual but everything else works fine.

  • Having interfaces for the proxied types is good practice. From a user standpoint, its just another object anyways and the implementation shouldn’t affect that.
  • extends Omit is a common pattern when you need to override the types of properties.
  • I submitted a PR (#85) to switch from void as a default type to unknown.
  • All the type helpers at the bottom look good.
0reactions
jakearchibaldcommented, Mar 15, 2019

Thanks for all the help here!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Jake Archibald on Twitter: "Any TypeScript experts willing to review ...
Any TypeScript experts willing to review my stupid code? ... Please review my (potentially) silly TypeScript · Issue #84 · jakearchibald/idb.
Read more >
TypeScript is a waste of time. Change my mind.
TypeScript unit tests are usually shorter and superior to JavaScript's due to there being no point to testing a bunch of silly stuff...
Read more >
Learn how to unleash the full potential of the type system of ...
Having types missing from @types/node is stupid. Many libraries lie about their types. You can act like typescript is some kind of magic...
Read more >
[AskJS] If you don't use TypeScript, tell me why (2 year follow up)
As Angular dev, I have to use TS. In my own projects, I either don't (visual studio code can read TS type data...
Read more >
Build Your First TypeScript Application (2018)
And, if I try to write something stupid, TypeScript will give me a relevant message and will refuse to compile (let alone run)...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found