Avatar ·

Как правильно реализовать константы класса в TypeScript?

Всем привет! Я изучаю TypeScript и столкнулся с проблемой при попытке определить константы внутри класса. Мне нужно создать класс, который будет хранить какие-то фиксированные значения, связанные с этим классом. Например, я хочу определить максимальное количество попыток входа (MAX_LOGIN_ATTEMPTS = 5) или статусы заказа (STATUS_PENDING = 'pending', STATUS_APPROVED = 'approved'). Я пробовал разные способы, но ни один не работает так, как я ожидаю: ```typescript class UserManager { // Попытка 1: просто свойство MAX_LOGIN_ATTEMPTS = 5; // Это свойство экземпляра, а не класса // Попытка 2: static с const static const MAX_LOGIN_ATTEMPTS = 5; // Ошибка синтаксиса // Попытка 3: readonly static readonly MAX_LOGIN_ATTEMPTS = 5; // Работает, но можно изменить через прототип } ``` Проблема в том, что: 1. Если я использую обычное свойство, оно создается для каждого экземпляра класса, что неэффективно. 2. `static const` не работает в TypeScript. 3. `static readonly` работает, но я слышал, что его все равно можно изменить в runtime через прототип класса. Как правильно определить настоящие константы, которые будут: - Принадлежать классу, а не экземпляру - Неизменяемыми (иммутабельными) - Доступными через имя класса: `UserManager.MAX_LOGIN_ATTEMPTS` И можно ли сделать так, чтобы константы были перечисляемыми, как в Java или C#? Буду благодарен за примеры кода! <<>>

Правильная реализация констант класса в TypeScript

В TypeScript нет прямого аналога static const из других языков, но есть несколько надежных способов реализовать константы класса. Давайте разберем лучшие практики.

Способ 1: static readonly (рекомендуемый базовый подход)

Комбинация static и readonly — это стандартный способ объявления констант класса:

class UserManager {
    // Константы класса
    static readonly MAX_LOGIN_ATTEMPTS = 5;
    static readonly DEFAULT_TIMEOUT = 3000;
    
    // Константы с явным типом
    static readonly STATUS_PENDING: string = 'pending';
    static readonly STATUS_APPROVED: string = 'approved';
    
    // Константы-объекты с readonly
    static readonly CONFIG = {
        apiUrl: 'https://api.example.com',
        version: 'v2'
    } as const;
    
    static checkLoginAttempts(attempts: number): boolean {
        return attempts <= UserManager.MAX_LOGIN_ATTEMPTS;
    }
}

// Использование
console.log(UserManager.MAX_LOGIN_ATTEMPTS); // 5
console.log(UserManager.STATUS_PENDING); // 'pending'

Почему это работает? readonly запрещает изменение свойства в TypeScript на этапе компиляции, а static делает его принадлежащим классу.

Способ 2: Полная защита с Object.freeze

Для runtime-защиты от изменений (даже через прототип) используйте Object.freeze:

class OrderManager {
    static readonly STATUSES = Object.freeze({
        PENDING: 'pending',
        APPROVED: 'approved',
        REJECTED: 'rejected',
        SHIPPED: 'shipped'
    } as const);
    
    static readonly MAX_ITEMS = Object.freeze(10);
    
    // Защита самого класса от расширения
    static {
        Object.freeze(OrderManager);
    }
}

// Попытка изменить выдаст ошибку в strict mode
// OrderManager.STATUSES.PENDING = 'new'; // TypeError в runtime
// OrderManager.MAX_ITEMS = 20; // TypeError в runtime

Способ 3: Enum-подобные константы (рекомендуется для групп констант)

Для набора связанных констант лучше использовать enum или namespace:

class PaymentProcessor {
    // Используем вложенный namespace для группировки
    static readonly Status = {
        PENDING: 'pending' as const,
        COMPLETED: 'completed' as const,
        FAILED: 'failed' as const
    } as const;
    
    // Альтернатива с enum внутри класса
    static readonly ErrorCode = {
        INVALID_AMOUNT: 1001,
        INSUFFICIENT_FUNDS: 1002,
        NETWORK_ERROR: 1003
    } as const;
    
    static processPayment(amount: number): string {
        if (amount <= 0) {
            return PaymentProcessor.ErrorCode.INVALID_AMOUNT.toString();
        }
        return PaymentProcessor.Status.COMPLETED;
    }
}

// Использование с type inference
type PaymentStatus = typeof PaymentProcessor.Status[keyof typeof PaymentProcessor.Status];
// PaymentStatus = 'pending' | 'completed' | 'failed'

Способ 4: Модульные константы (самый безопасный)

Для максимальной изоляции вынесите константы на уровень модуля:

// constants.ts
export const USER_CONSTANTS = {
    MAX_LOGIN_ATTEMPTS: 5,
    SESSION_TIMEOUT: 3600,
    ROLES: {
        ADMIN: 'admin',
        USER: 'user',
        GUEST: 'guest'
    }
} as const;

// user-manager.ts
import { USER_CONSTANTS } from './constants';

class UserManager {
    static readonly MAX_LOGIN_ATTEMPTS = USER_CONSTANTS.MAX_LOGIN_ATTEMPTS;
    
    static login(username: string, password: string): boolean {
        // Используем константы
        return true;
    }
}

Важные моменты:

  • readonly vs const: readonly для свойств класса, const для локальных переменных
  • as const: Сужает типы до литералов, делая объекты глубоко неизменяемыми на уровне типов
  • Object.freeze: Защищает от изменений в runtime, но не от TypeScript ошибок
  • static блоки: Позволяют выполнить инициализацию констант при загрузке класса

Рекомендация: Для простых случаев используйте static readonly. Для групп констант — as const с Object.freeze. Для перечислений — нативные enum TypeScript.

<<>>

Альтернативный подход: Использование абстрактного класса с enum

Если вам нужна строгая типизация и автодополнение в IDE, рассмотрите комбинацию enum с классом:

// Определяем enum для статусов
enum OrderStatus {
    PENDING = 'pending',
    APPROVED = 'approved',
    REJECTED = 'rejected'
}

// Класс использует enum как источник констант
abstract class OrderConstants {
    static readonly Status = OrderStatus;
    static readonly MAX_ITEMS = 50;
    static readonly MIN_AMOUNT = 10;
    
    // Запрещаем создание экземпляров
    private constructor() {}
}

// Использование
console.log(OrderConstants.Status.PENDING); // 'pending'
console.log(OrderConstants.MAX_ITEMS); // 50

// Type safety
function updateOrderStatus(status: OrderStatus) {
    // Только 'pending' | 'approved' | 'rejected'
}

Преимущества этого подхода:

  • enum обеспечивает строгую типизацию и автодополнение
  • Абстрактный класс с приватным конструктором предотвращает создание экземпляров
  • Константы логически сгруппированы внутри класса
  • TypeScript компилятор гарантирует неизменяемость enum значений

Недостаток: Enum компилируется в JavaScript объект, что добавляет небольшой overhead. Для pure констант лучше использовать as const.