Please review my (potentially) silly TypeScript
See original GitHub issueThis library takes the IndexedDB API, adds a couple of properties, and changes the return types to use promises rather than IDBRequest
s.
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:
- Created 5 years ago
- Comments:7 (6 by maintainers)
DBSchema
is a little unusual but everything else works fine.extends Omit
is a common pattern when you need to override the types of properties.void
as a default type tounknown
.Thanks for all the help here!