Типизация схемы в Dexie.js в TypeScript строится вокруг строгого описания структуры базы данных через интерфейсы, где каждая таблица получает явную модель данных, а вся схема связывается в единую типобезопасную конфигурацию. Такой подход позволяет вывести работу с IndexedDB на уровень статической проверки, устраняя целый класс ошибок, связанных с несоответствием структуры данных и запросов.
Типизация начинается с описания сущностей предметной области. Каждая таблица соответствует отдельному интерфейсу, который фиксирует форму записи.
interface User {
id: number;
name: string;
email: string;
age?: number;
}
interface Order {
id: number;
userId: number;
total: number;
createdAt: Date;
}
Ключевой момент заключается в том, что интерфейс отражает именно хранимое состояние, а не бизнес-логику. Это позволяет использовать один и тот же тип как для вставки, так и для чтения данных.
Типизация базы данных в Dexie.js строится через класс, наследующий
Dexie, где таблицы описываются как
Table<T, Key>.
import { Dexie, Table } fr om 'dexie';
class AppDatabase extends Dexie {
users!: Table<User, number>;
orders!: Table<Order, number>;
constructor() {
super('AppDatabase');
this.version(1).stores({
users: 'id, email, age',
orders: 'id, userId, createdAt'
});
}
}
Здесь возникает первый слой типизации:
User и Order фиксируют структуру
записиnumber задаёт тип первичного ключаTable<T, K> связывает схему и API таблицыСхема, передаваемая в stores, остаётся строковой, но её
смысл синхронизируется с интерфейсами вручную. Типизация не анализирует
строку индексов автоматически, поэтому поддержание консистентности
становится архитектурной задачей.
this.version(1).stores({
users: 'id, email, age',
});
Каждое поле индекса должно существовать в соответствующем интерфейсе. Несоответствие приводит к логическим ошибкам без компиляционных предупреждений.
При использовании составных индексов структура ключа усложняется, и типизация должна учитывать порядок и типы полей.
interface Enrollment {
studentId: number;
courseId: number;
enrolledAt: Date;
}
this.version(1).stores({
enrollments: '[studentId+courseId], enrolledAt'
});
Композитный индекс в TypeScript остаётся представленным как строка, но на уровне модели важно понимать, что ключ становится кортежной логикой доступа:
Dexie.js предоставляет Table<T, Key> как основной
механизм типизации операций CRUD.
async function addUser(db: AppDatabase, user: User) {
return db.users.add(user);
}
Тип User автоматически распространяется на:
addputgettoArraywhereЭто обеспечивает единообразие модели данных во всех операциях.
В случаях, когда ключ генерируется автоматически, используется тип
number или string с возможной опциональностью
поля.
interface LogEntry {
id?: number;
message: string;
timestamp: number;
}
class LogDB extends Dexie {
logs!: Table<LogEntry, number>;
constructor() {
super('LogDB');
this.version(1).stores({
logs: '++id, timestamp'
});
}
}
Символ ++ указывает автоинкремент, но TypeScript не
выводит это автоматически, поэтому поле id часто делается
необязательным.
Типизация запросов через where зависит от того, какие
поля объявлены как индексы.
db.users.where('age').above(18).toArray();
Если поле не объявлено в stores, TypeScript не сможет
ограничить доступ, и возникает риск неконсистентных запросов. Поэтому
схема индексов становится частью контрактной модели.
Транзакции усиливают типовую связность нескольких таблиц одновременно.
db.transaction('rw', db.users, db.orders, async () => {
const userId = await db.users.add({
id: 1,
name: 'Alex',
email: 'a@a.com'
});
await db.orders.add({
id: 1,
userId,
total: 100,
createdAt: new Date()
});
});
В TypeScript контекст транзакции сохраняет типы всех участвующих таблиц, предотвращая обращение к несуществующим полям или таблицам вне области транзакции.
Для крупных проектов используется единый интерфейс базы, описывающий все таблицы сразу.
interface DatabaseSchema {
users: User;
orders: Order;
logs: LogEntry;
}
Однако Dexie.js не требует этого интерфейса напрямую, поэтому он используется как архитектурный слой, а не как обязательная часть API.
Типизация может быть уточнена при работе с
select-подобными операциями через ручные utility-типы.
type UserPreview = Pick<User, 'id' | 'name'>;
async function getUserPreview(db: AppDatabase): Promise<UserPreview[]> {
return db.users.toArray().then(users =>
users.map(({ id, name }) => ({ id, name }))
);
}
Хотя библиотека не навязывает projection typing, TypeScript позволяет строить производные модели поверх базовых интерфейсов.
При изменении схемы важно синхронизировать интерфейсы с версионностью базы.
this.version(2).stores({
users: 'id, email, age, role'
});
Интерфейс:
interface UserV2 extends User {
role: string;
}
Подходы к типизации миграций:
Каждый вариант влияет на стабильность API доступа к данным.
Типизация в Dexie.js имеет фундаментальное ограничение: строковое описание индексов не связано напрямую с TypeScript-типами. Это приводит к следующим особенностям:
Эти ограничения компенсируются дисциплиной описания моделей и строгим разделением слоёв данных.
На практике создаётся слой репозитория, который инкапсулирует работу с таблицами.
class UserRepository {
constructor(private db: AppDatabase) {}
getByEmail(email: string) {
return this.db.users.where('email').equals(email).first();
}
}
Типы интерфейсов в этом случае формируют контракт между слоями хранения и бизнес-логики, сохраняя согласованность структуры данных без необходимости ручной проверки каждой операции.