Константы в классах TypeScript: от новичка до профессионала
Вы когда-нибудь писали на TypeScript и вдруг осознавали: «Эту магическую строку "ADMIN" я использую в десяти местах, а если опечатаюсь — компилятор промолчит»? Знакомая боль. Или, что ещё хуже, кто-то из команды меняет значение константы прямо в рантайме, и вы проводите час за дебагом, чтобы понять, почему всё сломалось.
Добро пожаловать в мир констант классов в TypeScript. Это не просто синтаксический сахар — это фундамент чистой, предсказуемой и безопасной архитектуры. В этой статье мы разберём все способы объявления констант в TypeScript-классах: от простого readonly до продвинутых техник с as const и дженериками. Поехали!
Почему константы в классах — это важно?
Представьте, что вы разрабатываете приложение для интернет-магазина. У вас есть класс Order, и в нём есть статусы: "pending", "shipped", "delivered". Если вы разбросаете эти строки по всему коду, рано или поздно кто-то напишет "PENDING" с большой буквы — и баг обеспечен. Константы классов TypeScript решают эту проблему раз и навсегда.
- Безопасность типов: компилятор не даст передать "pendingg" туда, где нужен "pending".
- Читаемость:
OrderStatus.Pendingпонятнее, чем просто строка. - Поддержка: изменение значения в одном месте меняет его везде.
Способ №1: readonly — база, но со нюансами
Самый простой способ — использовать модификатор readonly. Он запрещает изменение поля после инициализации.
class AppConfig {
readonly API_URL: string = 'https://api.example.com';
readonly MAX_RETRIES: number = 3;
}
const config = new AppConfig();
console.log(config.API_URL); // 'https://api.example.com'
// config.API_URL = 'new-url'; // Ошибка! Cannot assign to 'API_URL' because it is a read-only property.
Но есть подвох: readonly работает только на уровне TypeScript. В скомпилированном JavaScript эти поля всё ещё можно изменить. Для полной защиты используйте Object.freeze или # (приватные поля).
Способ №2: static readonly — король констант
Если константа не привязана к конкретному экземпляру класса (а так бывает в 99% случаев), используйте static readonly. Это классический class constant typescript паттерн.
class MathConstants {
static readonly PI: number = 3.14159;
static readonly E: number = 2.71828;
static readonly GOLDEN_RATIO: number = 1.61803;
}
// Доступ без создания экземпляра
console.log(MathConstants.PI); // 3.14159
// MathConstants.PI = 3.14; // Ошибка!
Этот подход идеален для конфигураций, математических констант, статусов и любых значений, которые не должны меняться.
Способ №3: enum — когда констант много
Если у вас группа связанных констант (например, статусы заказа), enum — ваш лучший друг. Он создаёт именованное множество значений.
enum OrderStatus {
Pending = 'pending',
Shipped = 'shipped',
Delivered = 'delivered',
Cancelled = 'cancelled'
}
class Order {
constructor(public status: OrderStatus) {}
}
const order = new Order(OrderStatus.Pending);
console.log(order.status); // 'pending'
// order.status = 'pendingg'; // Ошибка! Type '"pendingg"' is not assignable to type 'OrderStatus'
Совет: если вы используете строковые enum (как в примере), они дают читаемые значения в рантайме. Числовые enum быстрее, но их сложнее дебажить.
Способ №4: as const — для максимальной строгости
Иногда вы хотите, чтобы объект или массив стали константой с конкретными литеральными типами. Здесь на помощь приходит as const.
class HttpCodes {
static readonly codes = {
OK: 200,
NotFound: 404,
InternalError: 500
} as const;
}
// Тип: 200 | 404 | 500
type HttpCode = typeof HttpCodes.codes[keyof typeof HttpCodes.codes];
function handleResponse(code: HttpCode) {
if (code === HttpCodes.codes.OK) {
console.log('Success');
}
}
handleResponse(200); // Ок
// handleResponse(201); // Ошибка! Argument of type '201' is not assignable to parameter of type '200 | 404 | 500'
as const делает каждое свойство readonly и превращает значения в литеральные типы. Это мощный инструмент для создания типобезопасных конфигураций.
Способ №5: Приватные константы с # и геттеры
В современном TypeScript/JavaScript можно использовать приватные поля (#) вместе с геттерами для создания констант, которые нельзя изменить даже изнутри класса после инициализации.
class SecretConfig {
#secretKey: string;
constructor(key: string) {
this.#secretKey = key;
}
get secretKey(): string {
return this.#secretKey;
}
}
const config = new SecretConfig('my-secret');
console.log(config.secretKey); // 'my-secret'
// config.#secretKey = 'new'; // Ошибка! Property '#secretKey' is not accessible outside class
Этот подход полезен для конфиденциальных данных, которые должны быть доступны только для чтения извне.
Продвинутый паттерн: Константы с дженериками
Если вам нужно создавать классы с константами, которые зависят от типа, используйте дженерики.
class ConfigManager<T extends Record<string, string | number>> {
readonly config: T;
constructor(config: T) {
this.config = Object.freeze(config);
}
get<K extends keyof T>(key: K): T[K] {
return this.config[key];
}
}
const dbConfig = new ConfigManager({
host: 'localhost',
port: 5432,
database: 'mydb'
});
console.log(dbConfig.get('host')); // 'localhost'
// dbConfig.get('invalid'); // Ошибка! Argument of type '"invalid"' is not assignable to parameter of type '"host" | "port" | "database"'
Теперь вы можете создавать типобезопасные конфигурации для любых сценариев.
Лучшие практики и подводные камни
- Используйте UPPER_CASE для констант — это негласное правило, которое делает код читаемым:
static readonly MAX_VALUE = 100. - Не злоупотребляйте
enumдля простых случаев — если у вас всего две