Closed Jack-Works closed 3 years ago
I'm open to PRs to address this.
I have a typing for this.
function transaction<T extends readonly any[],
Mode extends "readonly" | "readwrite">
(storeNames: T, mode: Mode): {
writable: Mode extends "readwrite" ? true : boolean
_dont_use_or_youll_be_fired: Record<
T extends readonly (infer U)[] ? U : never
, never>
} {
return {} as any
}
{
let x = transaction(['a'] as const, 'readonly')
let y = transaction(['a'] as const, 'readwrite')
// readwrite transaction is okay for readonly transcation
x = y
// but the reverse order is wrong
y = x
}
{
let x = transaction(['a', 'b'] as const, 'readonly')
let y = transaction(['a'] as const, 'readonly')
// the transaction with fewer object store (y, with a)
// can't be assigned with transaction with more object store (x, with a b)
// or it will be a runtime error
x = y
// but the reverse order is wrong.
y = x
}
import { IDBPDatabase, DBSchema, StoreNames, IDBPTransaction, IDBPObjectStore } from 'idb'
export interface IDBPSafeObjectStore<
DBTypes extends DBSchema,
TxStores extends StoreNames<DBTypes>[] = StoreNames<DBTypes>[],
StoreName extends StoreNames<DBTypes> = StoreNames<DBTypes>
>
extends Omit<
IDBPObjectStore<DBTypes, TxStores, StoreName> /** createIndex is only available in versionchange transaction */,
| 'createIndex'
/** deleteIndex is only available in versionchange transaction */
| 'deleteIndex'
| 'transaction'
> {
[Symbol.asyncIterator](): AsyncIterableIterator<IDBPCursorWithValueIteratorValue<DBTypes, TxStores, StoreName>>
}
// don't use interface or typescript will not expand it to check assignibility
export type IDBPSafeTransaction<
DBTypes extends DBSchema,
TxStores extends StoreNames<DBTypes>[],
Mode extends 'readonly' | 'readwrite'
> = Omit<IDBPTransaction<DBTypes, TxStores>, 'objectStoreNames' | 'objectStore' | 'store'> & {
__writable__?: Mode extends 'readwrite' ? true : boolean
__stores__?: Record<
TxStores extends readonly (infer ValueOfUsedStoreName)[]
? ValueOfUsedStoreName extends string | number | symbol
? ValueOfUsedStoreName
: never
: never,
never
>
objectStore<StoreName extends TxStores[number]>(
name: StoreName,
): IDBPSafeObjectStore<DBTypes, StoreName[], StoreName>
}
export function createTransaction<DBType extends DBSchema, Mode extends 'readonly' | 'readwrite'>(
db: IDBPDatabase<DBType>,
mode: Mode,
) {
// It must be a high order function to infer the type of UsedStoreName correctly.
return <UsedStoreName extends StoreNames<DBType>[] = []>(
...storeNames: UsedStoreName
): IDBPSafeTransaction<DBType, UsedStoreName, Mode> => {
return db.transaction(storeNames, mode) as any
}
}
Consider the following code:
A transaction with access to
table1
andtable2
can be a subtype of a transaction with only access totable1
(y = x). But it seems to be invariant on the IDBPTransaction.And the mutatable of the transaction
readonly
/readwrite
is not reflected in the type IDBPTransaction, so it is possible to pass areadonly
transaction to a function that requires areadwrite
transaction then cause a runtime error.