Частые проблемы с типами и их решения

Одной из наиболее распространённых проблем при работе с 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"
});

В результате:

  • Dexie сохраняет новое поле email
  • TypeScript о нём «не знает»
  • доступ к 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 теряются: автодополнение, контроль полей и проверка операций отсутствуют.

Правильная типизация через generics

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

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 фактически существует в базе
  • TypeScript его запрещает
  • фильтрация и сортировка по 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, но типы проекта могут это не учитывать при неправильной декларации таблицы.

Решение: явное использование Partial

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);

Проблемы с Promise-типами и цепочками запросов

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;
}

Проблемы с readonly-типами

При использовании 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">>;

Потеря автодополнения в сложных проектах

При масштабировании приложения типы часто «разрываются» между слоями:

  • DB слой
  • сервисы
  • UI модели

Проблема

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
  };
}