Типизация поверх IndexedDB в связке с Dexie.js строится вокруг идеи описания структуры базы данных на уровне TypeScript, а затем связывания этих типов с экземпляром класса Dexie через наследование. Основной механизм расширения заключается в создании собственного класса, который наследуется от Dexie, с последующим описанием таблиц, их ключей и доменных моделей через обобщения.
Классическая схема начинается с объявления доменных интерфейсов и наследования Dexie:
import { Dexie, Table } fr om "dexie";
interface User {
id: number;
name: string;
email: string;
}
interface Order {
id: number;
userId: number;
total: number;
}
Далее создаётся класс базы данных, расширяющий Dexie, с явной типизацией таблиц:
class AppDatabase extends Dexie {
users!: Table<User, number>;
orders!: Table<Order, number>;
constructor() {
super("AppDatabase");
this.version(1).stores({
users: "id, email",
orders: "id, userId"
});
}
}
Здесь ключевой момент заключается в связке
Table<Entity, PrimaryKey>, которая позволяет
TypeScript выводить корректные типы для всех операций CRUD.
Внутренний механизм Dexie.js опирается на обобщённый тип
Table<T, TKey>, где:
T — модель данныхTKey — тип первичного ключаЭто позволяет получать строгую проверку при вызове методов:
const db = new AppDatabase();
await db.users.add({
id: 1,
name: "Alex",
email: "alex@mail.com"
});
Любое несоответствие структуре User приводит к ошибке
компиляции, что существенно снижает количество runtime-ошибок.
Расширение Dexie-класса позволяет добавлять доменные методы, инкапсулируя бизнес-логику прямо внутри слоя доступа к данным.
class AppDatabase extends Dexie {
users!: Table<User, number>;
constructor() {
super("AppDatabase");
this.version(1).stores({
users: "id, email"
});
}
async getUserByEmail(email: string) {
return this.users.wh ere("email").equals(email).first();
}
async createUser(user: User) {
return this.users.add(user);
}
}
Такой подход превращает экземпляр базы данных в полноценный repository-layer объект.
При добавлении методов важно учитывать контекст this,
поскольку Dexie использует собственную внутреннюю инициализацию таблиц
после конструктора. Поэтому доступ к таблицам возможен только после
super() и объявления версий.
Ошибочная попытка обращения к таблице до инициализации:
class BadDB extends Dexie {
users!: Table<User, number>;
constructor() {
super("BadDB");
this.users.add({ id: 1, name: "", email: "" }); // недопустимо
}
}
Корректная работа всегда предполагает использование методов после инициализации экземпляра.
При масштабных приложениях часто требуется расширять сам тип Dexie через module augmentation, чтобы интегрировать дополнительные поля или плагины.
declare module "dexie" {
interface Dexie {
readonly versionNumber: number;
}
}
После этого расширение становится частью типовой системы всего проекта.
Более строгий подход заключается в описании всей схемы базы через единый интерфейс:
interface DBSchema {
users: User;
orders: Order;
}
И последующем связывании с Dexie:
class AppDatabase extends Dexie {
users!: Table<DBSchema["users"], number>;
orders!: Table<DBSchema["orders"], number>;
}
Такой способ снижает дублирование типов и обеспечивает централизованное управление схемой.
Dexie.js поддерживает транзакции, которые можно оборачивать в строго типизированные методы класса:
class AppDatabase extends Dexie {
users!: Table<User, number>;
orders!: Table<Order, number>;
constructor() {
super("AppDatabase");
this.version(1).stores({
users: "id, email",
orders: "id, userId"
});
}
async createUserWithOrder(user: User, order: Order) {
return this.transaction("rw", this.users, this.orders, async () => {
await this.users.add(user);
await this.orders.add(order);
});
}
}
Типизация гарантирует, что таблицы участвуют в транзакции корректно и доступны внутри callback.
Наследование позволяет внедрять общие механизмы логирования, кеширования или валидации:
class BaseDatabase extends Dexie {
protected log(query: string) {
console.log("DB QUERY:", query);
}
}
class AppDatabase extends BaseDatabase {
users!: Table<User, number>;
constructor() {
super("AppDatabase");
this.version(1).stores({
users: "id, email"
});
}
async findUser(id: number) {
this.log(`users.get(${id})`);
return this.users.get(id);
}
}
Такой подход полезен при построении архитектуры с единым слоем доступа к данным.
При расширении Dexie-класса важно синхронизировать типы с индексами
схемы stores. Несоответствие индексов приводит к логическим
ошибкам, которые TypeScript не всегда способен отловить.
this.version(1).stores({
users: "++id, email, name",
});
Здесь ++id обозначает автоинкремент, а тип
number должен отражать это поведение:
users!: Table<User, number>;
В более сложных архитектурах допускается оборачивание Dexie-экземпляра в сервисный класс, но при этом сохраняется типизированный доступ:
class DatabaseService {
private db: AppDatabase;
constructor() {
this.db = new AppDatabase();
}
get users() {
return this.db.users;
}
async allUsers() {
return this.db.users.toArray();
}
}
Такой подход снижает связанность, сохраняя преимущества строгой типизации Dexie.js.
Для масштабируемых приложений часто применяется фабрика, создающая типизированные экземпляры Dexie:
function createDatabase<TSchema>() {
class GenericDB extends Dexie {
constructor(name: string) {
super(name);
}
}
return GenericDB;
}
Хотя TypeScript не может полностью вывести структуру таблиц без явного описания, этот подход используется как основа для динамической генерации схем.
Расширение класса всегда должно учитывать:
Dexie.js при этом выступает как слой абстракции над IndexedDB, а расширение класса превращает его в типизированный persistence-layer, пригодный для крупных приложений с предсказуемой структурой данных