jakearchibald / idb

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

TypeScript problem: Wrong variation rule on transaction #140

Closed Jack-Works closed 3 years ago

Jack-Works commented 4 years ago

Consider the following code:

declare let x: IDBPTransaction<MyDB, ['table1' | 'table2']>
declare let y: IDBPTransaction<MyDB, ['table1']>
x = y // Incompatiable type. That's right.
y = x // Incompatible type too, that's not right.

A transaction with access to table1 and table2 can be a subtype of a transaction with only access to table1 (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 a readonly transaction to a function that requires a readwrite transaction then cause a runtime error.

jakearchibald commented 4 years ago

I'm open to PRs to address this.

Jack-Works commented 4 years ago

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
}
Jack-Works commented 4 years ago
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
    }
}