Dexie.js предоставляет поверх IndexedDB мощный слой типизированного
доступа к данным, и именно использование generics в TypeScript
превращает её из просто удобной обёртки в строго типизированное
хранилище данных. Обобщения пронизывают ключевые элементы: описание
таблиц, типизацию записей, результат запросов и цепочки методов
Collection. Правильное использование generics позволяет
добиться полной статической проверки структуры базы данных на этапе
компиляции.
Dexie строится вокруг описания структуры базы через расширение класса
Dexie. Основой является сопоставление имени таблицы и типа
сущности.
interface User {
id: number;
name: string;
age: number;
}
class MyDatabase extends Dexie {
users!: Table<User, number>;
constructor() {
super("myDatabase");
this.version(1).stores({
users: "++id, name, age"
});
}
}
Здесь Table<User, number> — ключевой пример
использования generics:
User задаёт форму записиnumber определяет тип первичного
ключаЭта связь формирует основу типобезопасности во всех последующих операциях.
Тип Table<T, Key> определяет поведение таблицы как
коллекции объектов типа T, индексированных ключом
Key.
T определяет форму данныхKey задаёт тип primary keyT и
KeyПример операций:
await db.users.add({
name: "Alice",
age: 30
});
TypeScript автоматически проверяет соответствие структуры
User. Отсутствие id допустимо, если ключ
автогенерируемый (++id).
Каждый метод Table<T, Key> строго использует тип
T.
add(item: Omit<T, Key>): Promise<Key>
В случае автоинкремента ключ исключается из входного объекта.
get(key: Key): Promise<T | undefined>
Возвращаемое значение строго привязано к типу записи.
update(key: Key, changes: Partial<T>): Promise<number>
Тип Partial<T> позволяет передавать только
изменяемые поля без полной структуры объекта.
После выполнения запросов Dexie возвращает объект
Collection<T>, который является ключевым элементом
для построения цепочек фильтрации и сортировки.
const adults = db.users
.where("age")
.above(18)
.toArray();
Здесь тип T сохраняется на всём протяжении цепочки.
Метод where() не изменяет тип коллекции, но ограничивает
выборку:
where(index: keyof T): Collection<T>
TypeScript гарантирует, что индекс существует в T.
Попытка обратиться к несуществующему полю приводит к ошибке
компиляции.
Методы Collection<T> используют generics для
сохранения строгой типизации на каждом шаге:
filter(fn: (value: T) => boolean): Collection<T>
Пример:
const result = db.users
.filter(user => user.age > 18)
.toArray();
Тип user внутри функции строго выводится как
User.
orderBy(index: keyof T): Collection<T>
Сортировка возможна только по ключам, определённым в типе.
Эти методы не изменяют тип:
limit(n: number): Collection<T>
offset(n: number): Collection<T>
Тип данных остаётся T, что важно для сохранения
цепочки.
Некоторые методы позволяют изменять тип результата, что делает generics более гибкими.
map<U>(fn: (value: T) => U): Collection<U>
Пример:
const names = db.users
.toCollection()
.map(user => user.name);
Результат: Collection<string>.
Методы вроде modify и delete не меняют тип
коллекции, но оперируют Key:
modify(changes: Partial<T>): Promise<number>
delete(key: Key): Promise<void>
При использовании нескольких условий тип T остаётся
неизменным, однако логическая структура запроса становится более
ограниченной.
db.users
.where("age")
.above(18)
.and(user => user.name.startsWith("A"))
Здесь and сохраняет Collection<T> без
изменения типа, но добавляет дополнительный предикат:
and(fn: (value: T) => boolean): Collection<T>
Финальные методы коллекций завершают цепочку и возвращают строго типизированные результаты.
toArray(): Promise<T[]>
first(): Promise<T | undefined>
last(): Promise<T | undefined>
Эти методы фиксируют типизацию, превращая ленивую коллекцию в конкретные данные.
Dexie позволяет использовать диапазоны ключей, сохраняя тип ключа
Key.
where("id")
.between(10, 20)
Сигнатура:
between(lower: Key, upper: Key): Collection<T>
Это обеспечивает строгую проверку границ диапазона.
Когда используется where, TypeScript ограничивает выбор
полей только ключами T:
where(index: keyof T)
Это исключает возможность обращения к несуществующим полям и гарантирует согласованность схемы.
В сложных архитектурах можно расширять типизацию через композицию:
type BaseEntity = {
id: number;
};
type Auditable = {
createdAt: Date;
updatedAt: Date;
};
type User = BaseEntity & Auditable & {
name: string;
};
Dexie корректно обрабатывает такие объединённые типы:
Table<User, number>
Все поля становятся частью типизированных запросов.
Все операции Dexie асинхронны, но generics сохраняются через
Promise<T>:
Promise<T>Promise<T[]>Promise<Key>TypeScript выводит тип результата на основе исходной коллекции:
const user: User | undefined = await db.users.get(1);
Type inference играет ключевую роль: Dexie не требует явного указания типов на каждом шаге цепочки, но сохраняет их через generics:
const q = db.users
.where("age")
.above(18)
.filter(u => u.name.length > 3)
.map(u => ({ id: u.id, name: u.name }));
Тип результата автоматически становится:
Collection<{ id: number; name: string }>
Несмотря на мощную типизацию, существуют ограничения:
stores) не типизируется автоматически
через TЭти ограничения связаны с природой IndexedDB, а не TypeScript.
В транзакциях типизация сохраняется без изменений:
db.transaction("rw", db.users, async () => {
const user = await db.users.get(1);
await db.users.put({ ...user!, name: "New" });
});
Тип Table<T, Key> не теряет информации внутри
транзакционного контекста, а Collection<T> продолжает
функционировать как типизированная цепочка.
Типичная цепочка:
const result = await db.users
.where("age")
.above(21)
.filter(u => u.name.includes("a"))
.toArray();
Тип результата:
User[]
Каждый этап сохраняет или трансформирует T, но никогда
не теряет его семантику.
Внутренняя структура типизации Dexie фактически представляет собой систему зависимых generics:
Table<T, Key> → источник типовCollection<T> → потоковая трансформацияT → T или
T → UЭта модель обеспечивает строгую типовую консистентность на всём пути от хранения до выборки данных