Работа с TypeScript: типы, интерфейсы, декораторы

При использовании bcrypt.js в TypeScript основной сложностью становится отсутствие встроенной строгой типизации в самой библиотеке. Решается это подключением внешних типов и правильной организацией обёрток над асинхронными функциями хеширования и проверки паролей.

Установка типов выполняется отдельно:

npm install bcryptjs
npm install --save-dev @types/bcryptjs

После этого библиотека начинает корректно работать в типизированной среде, однако важно учитывать, что типы описывают только сигнатуры функций, а не поведение на уровне бизнес-логики.


Базовые типы и обёртки

bcrypt.js предоставляет два основных сценария работы: синхронный и асинхронный. В TypeScript предпочтение почти всегда отдаётся асинхронному варианту из-за блокировки event loop в синхронных вызовах.

Типизированная обёртка для хеширования пароля обычно выглядит следующим образом:

import bcrypt from "bcryptjs";

export interface IPasswordHasher {
  hash(password: string, saltRounds: number): Promise<string>;
  compare(password: string, hash: string): Promise<boolean>;
}

export class BcryptHasher implements IPasswordHasher {
  async hash(password: string, saltRounds: number): Promise<string> {
    return bcrypt.hash(password, saltRounds);
  }

  async compare(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }
}

Здесь важно, что интерфейс фиксирует контракт, а реализация может быть заменена без изменения остального кода.


Типизация пользовательских моделей

В большинстве приложений bcrypt используется в связке с сущностью пользователя. Типизация позволяет строго отделить сырой пароль от уже захешированного значения.

export interface IUser {
  id: string;
  email: string;
  password: string;
}

На уровне доменной модели часто вводят дополнительное разделение:

export interface ICreateUserDTO {
  email: string;
  password: string;
}

export interface IStoredUser {
  id: string;
  email: string;
  passwordHash: string;
}

Такое разделение предотвращает случайное сохранение открытого пароля в базу данных.


Сервисный слой с типами

При использовании TypeScript логика работы с bcrypt обычно выносится в сервис:

export class AuthService {
  constructor(private readonly hasher: IPasswordHasher) {}

  async register(dto: ICreateUserDTO): Promise<IStoredUser> {
    const passwordHash = await this.hasher.hash(dto.password, 10);

    return {
      id: crypto.randomUUID(),
      email: dto.email,
      passwordHash
    };
  }

  async validatePassword(
    password: string,
    passwordHash: string
  ): Promise<boolean> {
    return this.hasher.compare(password, passwordHash);
  }
}

Типизация здесь выполняет две ключевые функции:

  • фиксирует структуру входных данных
  • защищает от передачи «сырого» пароля дальше по системе

Декораторы для автоматического хеширования

TypeScript-декораторы позволяют внедрять поведение на уровне методов и классов. В контексте bcrypt.js они часто применяются для автоматического хеширования пароля перед сохранением сущности.

Пример метода-декоратора:

function HashPassword(saltRounds = 10) {
  const hasher = new BcryptHasher();

  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const user = args[0];

      if (user?.password) {
        user.passwordHash = await hasher.hash(user.password, saltRounds);
        delete user.password;
      }

      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

Использование в сервисе:

class UserService {
  @HashPassword(12)
  async createUser(user: { email: string; password: string }) {
    // сюда уже приходит объект с passwordHash вместо password
    return user;
  }
}

Такой подход позволяет вынести криптографическую логику из бизнес-кода, сохранив при этом строгую типизацию входных данных.


Типизация контекста декораторов

При работе с декораторами в TypeScript часто возникает проблема потери типизации this. Решается это через явное описание контекста:

type UserInput = {
  email: string;
  password?: string;
  passwordHash?: string;
};

И применение в декораторе:

descriptor.value = async function (this: any, user: UserInput) {
  ...
};

Более строгий вариант предполагает создание базового интерфейса сервиса:

interface IUserService {
  createUser(user: UserInput): Promise<UserInput>;
}

Безопасность типов при работе с bcrypt

TypeScript не контролирует содержимое строк, поэтому даже при наличии типов возможны ошибки на уровне логики. Основные уязвимости возникают в следующих случаях:

  • хранение password вместо passwordHash
  • повторное хеширование уже захешированного значения
  • передача any вместо строгих DTO

Для защиты часто вводят дополнительный тип-страж:

function isHashed(value: string): boolean {
  return value.startsWith("$2a$") || value.startsWith("$2b$");
}

И использование его в логике:

if (!isHashed(user.password)) {
  user.passwordHash = await bcrypt.hash(user.password, 10);
}

Интеграция с ORM и строгие интерфейсы

При работе с ORM (например, Sequelize или TypeORM) типизация bcrypt используется на уровне entity-классов:

export class UserEntity {
  id!: string;
  email!: string;
  passwordHash!: string;

  setPassword(password: string) {
    this.passwordHash = bcrypt.hashSync(password, 10);
  }

  validatePassword(password: string): boolean {
    return bcrypt.compareSync(password, this.passwordHash);
  }
}

В TypeScript важно избегать хранения сырого пароля в entity, даже временно. Поэтому часто вводят отдельные DTO-классы для входящих данных.


Обобщённые типы для криптографических сервисов

Для расширяемости можно абстрагировать bcrypt через дженерики:

export interface IHasher<TInput = string, TOutput = string> {
  hash(input: TInput): Promise<TOutput>;
  compare(input: TInput, hashed: TOutput): Promise<boolean>;
}

Это позволяет в будущем заменить bcrypt на argon2 или другой алгоритм без изменения контрактов приложения.


Практическая архитектура с типизацией и декораторами

В зрелых приложениях TypeScript, bcrypt и декораторы формируют единый слой безопасности:

  • DTO описывает входные данные
  • сервис содержит бизнес-логику
  • bcrypt изолирован в отдельном хешере
  • декораторы автоматизируют преобразование данных
  • интерфейсы фиксируют контракты между слоями

Такое разделение уменьшает связность кода и делает криптографическую часть предсказуемой и тестируемой без привязки к конкретной реализации библиотеки.