Опирается на правила:
NODE-22…NODE-24из Node Style Guide → раздел 6. Иммутабельность.
Важно знать
readonlyна полях — любое поле, которое не переприсваивается после конструктора, обязано бытьreadonly.- DI-зависимости NestJS — объявляются как
private readonlyпрямо в параметрах конструктора; шаблонный код отсутствует.as const— для литеральных констант и конфиг-объектов; без него TypeScript расширяет тип доstring/number.readonly T[]в публичных сигнатурах — когда метод возвращает коллекцию и мутация снаружи не предполагается.- Spread /
structuredClone— единственный способ «изменить» объект; входные аргументы не мутируются никогда.varзапрещён (NODE-X10):constпо умолчанию,letтолько когда значение реально переприсваивается.- Механику (
constvslet,no-param-reassign) ловит ESLint; семантику (когда нуженreadonly, когда spread) — ревью.strictPropertyInitializationвtsconfig.json(strict: true) гарантирует, чтоreadonly-поле инициализировано в конструкторе или объявлении.
Иммутабельность — дешевейший способ устранить целый класс ошибок: когда объект нельзя изменить после создания, его поведение предсказуемо в любой точке кода. TypeScript даёт для этого readonly и as const на уровне системы типов; runtime-защита — через spread и structuredClone. Три правила раздела закрывают три уровня: поля класса, литеральные значения, мутация аргументов.
readonly на полях класса
NODE-22: поле, которое не переприсваивается после конструктора, объявляется readonly.
class OrderProjection {
readonly id: string;
readonly customerId: string;
readonly totalAmount: bigint;
readonly createdAt: Date;
constructor(id: string, customerId: string, totalAmount: bigint, createdAt: Date) {
this.id = id;
this.customerId = customerId;
this.totalAmount = totalAmount;
this.createdAt = createdAt;
}
}
readonly фиксирует намерение: «этот объект описывает снимок состояния, его поля не изменятся». Любая попытка присвоить projection.id = '...' — ошибка компиляции, а не рантайм-сюрприз.
Для Record-образных VO TypeScript предоставляет утилитарный тип Readonly<T>:
type ProductSnapshot = Readonly<{
id: string;
name: string;
priceKopecks: bigint;
sku: string;
}>;
Readonly<T> делает все поля readonly без перечисления каждого вручную — удобно для объектов данных без методов.
DI-зависимости — private readonly в параметрах
NODE-22: зависимости NestJS объявляются через сокращённый синтаксис параметров конструктора.
@Injectable()
export class CreateOrderHandler {
constructor(
@Inject(ORDER_REPOSITORY)
private readonly orderRepository: OrderRepository,
@Inject(CUSTOMER_REPOSITORY)
private readonly customerRepository: CustomerRepository,
private readonly eventBus: EventBus,
) {}
async execute(command: CreateOrderCommand): Promise<OrderId> {
const customer = await this.customerRepository.findById(command.customerId);
const order = Order.create(customer, command.items);
await this.orderRepository.save(order);
await this.eventBus.publish(new OrderCreatedEvent(order.id));
return order.id;
}
}
private readonly в параметре конструктора — TypeScript-сокращение: TypeScript одновременно объявляет поле и присваивает его в конструкторе. Трёх строк вместо шести. readonly здесь не опция, а требование: зависимость не должна быть переприсвоена в течение жизни объекта.
Объявление поля отдельно от конструктора — антипаттерн:
@Injectable()
export class CreateOrderHandler {
private orderRepository: OrderRepository;
constructor(@Inject(ORDER_REPOSITORY) repo: OrderRepository) {
this.orderRepository = repo;
}
}
Это то же самое, только длиннее и без readonly. ESLint-правила @typescript-eslint/parameter-properties (опция prefer: ['private readonly']) и @typescript-eslint/prefer-readonly автоматизируют контроль.
as const для литеральных констант и конфиг-объектов
NODE-23: as const сужает тип литерала до точного значения.
Без as const TypeScript расширяет тип:
const direction = 'left';
Тип direction — string. TypeScript «предполагает», что переменная может быть переприсвоена другой строкой, поэтому тип широкий. С as const:
const direction = 'left' as const;
Тип — 'left'. Значение зафиксировано на уровне системы типов.
Для конфиг-объектов и наборов значений as const незаменим:
export const ORDER_STATUS = {
NEW: 'new',
CONFIRMED: 'confirmed',
PAID: 'paid',
CANCELLED: 'cancelled',
} as const;
export type OrderStatus = (typeof ORDER_STATUS)[keyof typeof ORDER_STATUS];
OrderStatus — 'new' | 'confirmed' | 'paid' | 'cancelled'. Добавили новый статус в ORDER_STATUS — тип обновился автоматически. Никаких enum с непредсказуемым runtime-поведением (cross-ref NODE-X12).
Для конфигурации сервиса:
export const SBER_PAYMENT_CONFIG = {
timeoutMs: 5000,
maxRetries: 3,
baseUrl: 'https://api.sber.ru/v1/payments',
} as const;
Без as const тип полей — number/string. С as const — 5000, 3, 'https://...'. Попытка переписать поле — ошибка компиляции.
readonly T[] в публичных сигнатурах
NODE-23: когда метод возвращает коллекцию и мутация снаружи не предполагается, тип аннотируется как readonly T[] или ReadonlyArray<T>.
interface OrderRepository {
findByCustomerId(customerId: string): Promise<readonly Order[]>;
}
class OrderQueryService {
async getCustomerOrders(customerId: string): Promise<readonly OrderSummary[]> {
const orders = await this.orderRepository.findByCustomerId(customerId);
return orders.map(OrderSummary.from);
}
}
readonly Order[] не запрещает мутацию самих объектов Order — он запрещает push, pop, splice, прямое присвоение по индексу. Вызывающий код не может случайно сломать чужой кэш или коллекцию.
Разница типов:
const orders: readonly Order[] = [];
orders.push(order); // ошибка компиляции
orders[0] = order; // ошибка компиляции
const orders: Order[] = [];
orders.push(order); // допустимо — нежелательный side-effect
Spread и structuredClone вместо мутации
NODE-24: входные аргументы функции не мутируются никогда. Для создания изменённой версии используются spread-оператор или structuredClone.
Мутация аргумента — одна из самых трудноуловимых ошибок: функция с виду не возвращает ничего нового, но изменяет объект, которым владеет вызывающий код.
function applyDiscount(order: Order, discountPercent: number): Order {
return {
...order,
totalAmount: order.totalAmount - (order.totalAmount * BigInt(discountPercent)) / 100n,
};
}
Spread создаёт новый объект с изменённым полем; исходный order остаётся нетронутым. Вызывающий код хранит оригинал и получает новый объект — оба валидны.
Для глубокого клонирования — structuredClone:
function enrichProduct(product: Product, attributes: Record<string, string>): Product {
const clone = structuredClone(product);
Object.assign(clone.attributes, attributes);
return clone;
}
structuredClone — стандарт Web API, доступен в Node.js начиная с v17. Клонирует глубоко, включая Date, Map, Set, типизированные массивы. Не клонирует функции и прототипы — только данные. Для объектов с методами (классовые инстанции) — spread достаточен.
Для иммутабельного обновления вложенных объектов:
function updateShippingAddress(
order: Order,
newAddress: Address,
): Order {
return {
...order,
shipping: {
...order.shipping,
address: newAddress,
},
};
}
Каждый уровень вложенности — отдельный spread. Глубже двух уровней — сигнал, что структура данных требует пересмотра.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Поле без readonly при отсутствии переприсваивания | NODE-22 | readonly id: string |
private service: SomeService в конструкторе без readonly | NODE-22 | private readonly service: SomeService |
const config = { timeout: 5000 } без as const | NODE-23 | { timeout: 5000 } as const |
Order[] в возвращаемом типе публичного метода, если мутация не нужна | NODE-23 | readonly Order[] |
argument.field = newValue внутри функции | NODE-24 | return { ...argument, field: newValue } |
argument.items.push(item) | NODE-24 | return { ...argument, items: [...argument.items, item] } |
var count = 0 | NODE-X10 | let count = 0 (или const если не переприсваивается) |
Куда дальше
- node/naming.md —
UPPER_SNAKE_CASEдля констант уровня модуля и DI-токенов. - node/imports.md — named exports и path-aliases; как модуль связан с тем, что он экспортирует.
- node/expressions.md —
unknown+ narrowing, guard clause, явные возвращаемые типы. - node/async.md —
async/await,Promise.all, запрет fire-and-forget. - node/tooling.md —
tsconfig.jsonstrict, ESLint strictTypeChecked, CI-прогон. - Раздел «Стандарты → Code Style» — хаб языковых биндингов: Java, Node, Python, Go.