При использовании 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>;
}
TypeScript не контролирует содержимое строк, поэтому даже при наличии типов возможны ошибки на уровне логики. Основные уязвимости возникают в следующих случаях:
password вместо passwordHashany вместо строгих 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 (например, 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 и декораторы формируют единый слой безопасности:
Такое разделение уменьшает связность кода и делает криптографическую часть предсказуемой и тестируемой без привязки к конкретной реализации библиотеки.