Одной из наиболее распространённых проблем при работе с Dexie.js в TypeScript становится расхождение между реальной структурой IndexedDB и описанными типами данных. Dexie не синхронизирует типы автоматически — вся типизация является статической и существует только на уровне компиляции.
Типичная ошибка возникает, когда схема таблицы изменяется, а интерфейсы остаются прежними:
interface User {
id: number;
name: string;
}
db.version(1).stores({
users: "++id,name"
});
Позже добавляется поле email, но интерфейс не обновляется:
db.version(2).stores({
users: "++id,name,email"
});
В результате:
email
user.email приводит к ошибкам типизации
Наиболее надёжный подход — связывать интерфейсы и схему через один слой абстракции:
interface User {
id: number;
name: string;
email?: string;
}
Ключевой момент — обязательное обновление типов при каждой миграции схемы.
any в таблицах
Частая ошибка — объявление базы без строгой типизации:
const db = new Dexie("app");
db.version(1).stores({
users: "++id,name"
});
const users = db.table("users"); // any
В этом случае все преимущества TypeScript теряются: автодополнение, контроль полей и проверка операций отсутствуют.
Dexie поддерживает строгую типизацию таблиц:
interface User {
id: number;
name: string;
}
class MyDB extends Dexie {
users!: Dexie.Table<User, number>;
constructor() {
super("app");
this.version(1).stores({
users: "++id,name"
});
}
}
const db = new MyDB();
Теперь:
users.add() принимает строго User
users.get() возвращает User | undefined
keyPath)
Dexie различает primary key и обычные поля. Несоответствие типов часто возникает при неправильном описании ключа.
interface User {
id: string; // ошибка ожидания number
name: string;
}
this.version(1).stores({
users: "++id,name"
});
Dexie ожидает, что id будет number, потому что
++ означает автоинкремент.
Тип ключа должен соответствовать схеме:
interface User {
id?: number;
name: string;
}
Ключевой момент:
number | undefined
uuid без ++
Dexie не различает «отсутствующее поле» и undefined на
уровне хранения, но TypeScript — различает.
interface User {
id: number;
name: string;
email: string; // обязательное
}
Если запись добавляется без email:
db.users.add({ id: 1, name: "Alex" });
TypeScript выдаёт ошибку.
IndexedDB спокойно хранит объект без поля email.
Использование optional-полей:
interface User {
id: number;
name: string;
email?: string;
}
Dexie версии и TypeScript-типы легко расходятся при росте приложения.
db.version(1).stores({
users: "++id,name"
});
db.version(2).stores({
users: "++id,name,age"
});
Но интерфейс остаётся старым:
interface User {
id: number;
name: string;
}
age фактически существует в базе
age ломается на уровне типов
interface User {
id: number;
name: string;
age?: number;
}
При сложных схемах применяется версионирование типов:
interface UserV1 {
id: number;
name: string;
}
interface UserV2 extends UserV1 {
age: number;
}
update, patch и
частичных типов
Методы обновления часто приводят к проблемам из-за строгих типов.
db.users.update(1, {
name: "New Name"
});
TypeScript ожидает полный User, а не частичное обновление.
Метод update использует Partial<T>
внутри Dexie, но типы проекта могут это не учитывать при неправильной
декларации таблицы.
db.table<User, number>("users").update(1, {
name: "New Name"
} as Partial<User>);
Или более чисто:
type UserUpdate = Partial<User>;
db.users.update(1, {
name: "New Name"
} as UserUpdate);
Dexie активно использует Promise, но неправильная типизация приводит к потере вывода типов.
const result = db.users.toArray();
Если таблица не типизирована, result становится
any[].
const result: User[] = await db.users.toArray();
или через строгую таблицу:
const result = await db.users.toArray();
// result: User[]
collection
Dexie позволяет строить запросы через where,
filter, toCollection, но цепочки могут терять
типизацию.
db.users.where("name").equals("Alex").toArray();
Иногда TypeScript не выводит тип результата корректно, особенно при сложных индексах.
Отсутствие строгого связывания индексов и интерфейса.
Явное указание типа таблицы:
const users = db.table<User, number>("users");
const result = await users.where("name").equals("Alex").toArray();
Dexie поддерживает compound indexes:
"++id,name,age"
TypeScript не проверяет корректность составного ключа.
db.users.where("name,age") // строка не типизируется
Ошибки проявляются только в runtime.
Использование строго типизированных ключей через generics и вспомогательные типы:
type UserIndexes = "id" | "name" | "age";
И ограничение доступа:
where(index: UserIndexes)
null и undefined
Dexie хранит данные в IndexedDB, где null и
undefined ведут себя по-разному.
interface User {
email?: string;
}
Запись:
email: null
TypeScript считает это ошибкой, но база допускает.
Явное расширение типа:
interface User {
email?: string | null;
}
При использовании TypeScript часто добавляют readonly:
interface User {
readonly id: number;
name: string;
}
db.users.update(1, { id: 2 });
TypeScript запрещает изменение id, хотя Dexie позволяет
логически передать объект.
Разделение моделей:
type User = {
readonly id: number;
name: string;
};
type UserUpdate = Partial<Omit<User, "id">>;
При масштабировании приложения типы часто «разрываются» между слоями:
function getUser(id: number) {
return db.users.get(id);
}
Возвращаемый тип становится Promise<any> при неверной
конфигурации.
Централизованное объявление базы:
class AppDB extends Dexie {
users!: Dexie.Table<User, number>;
}
И использование этого класса везде как единственного источника типов.
Dexie хранит данные как structured clone, что иногда приводит к неожиданным типам:
Date → сохраняется, но возвращается как Date
Map, Set → поддерживаются частично
class User {
constructor(public name: string) {}
greet() {
return "Hello " + this.name;
}
}
После загрузки:
const user = await db.users.get(1);
// user.greet is undefined
Использование DTO-моделей:
interface User {
id: number;
name: string;
}
function toUserModel(u: User) {
return {
...u,
greet: () => "Hello " + u.name
};
}