Опирается на правила:
NODE-15…NODE-19из Node Style Guide → раздел 4. Выражения и типизация.
Важно знать
- Граничные данные (
req.body, ответ внешнего API, данные из очереди) — типunknown, неany; сужение через type guard или схему-валидатор.- Возвращаемый тип публичных функций и методов — всегда аннотирован явно: фиксирует контракт, ловит случайное расширение типа.
- Guard clause — ранний
throw/returnв начале функции; вложенныеif/elseзапрещены как основная структура.- Деньги —
bigint(минорные единицы, например копейки) или Decimal-библиотека;numberтеряет точность при операциях с плавающей точкой.- Время — UTC везде (хранение, передача, вычисления); конвертация в локальное время — только на уровне представления.
- Булево выражение — не более 3 операторов
&&/||; сложнее — выносится в именованный предикат.any(явный или черезas any) отравляет типизацию всего downstream-кода — запрещён (NODE-X5).- Non-null assertion (
x!) скрывает реальный nullability; заменяется на narrowing,??или явный| undefinedв типе (NODE-X6).
Правильная типизация — это не формальность ради компилятора. Она делает контракт функции читаемым без просмотра реализации, обнаруживает ошибки при сборке, а не в рантайме на проде, и исключает класс ошибок, которые any намеренно скрывает.
Граничные данные — unknown и narrowing
NODE-15: всё, что приходит снаружи системы — тип unknown. Использовать только после явного сужения.
Граничные точки: HTTP-тело запроса, ответ внешнего сервиса, сообщение из Kafka, данные из Redis-кэша без схемы, параметры вебхука.
import { z } from 'zod';
const CreateOrderSchema = z.object({
customerId: z.string().uuid(),
items: z.array(
z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
}),
),
});
type CreateOrderCommand = z.infer<typeof CreateOrderSchema>;
@Controller('orders')
export class OrderController {
@Post()
async createOrder(@Body() body: unknown): Promise<{ orderId: string }> {
const command = CreateOrderSchema.parse(body);
return this.createOrderHandler.execute(command);
}
}
CreateOrderSchema.parse(body) бросает ZodError при несоответствии — контракт нарушен на входе, а не в глубине обработчика.
Если схема не нужна, но тип необходимо сузить — type guard:
function isOrderCreatedEvent(value: unknown): value is OrderCreatedEvent {
return (
typeof value === 'object' &&
value !== null &&
'orderId' in value &&
'customerId' in value
);
}
async handleEvent(payload: unknown): Promise<void> {
if (!isOrderCreatedEvent(payload)) {
throw new InvalidEventPayloadError('OrderCreatedEvent', payload);
}
await this.processOrderCreated(payload);
}
Явные возвращаемые типы
NODE-16: публичные функции и методы аннотируются явно — даже когда TypeScript способен вывести тип самостоятельно.
export class CreateOrderHandler {
async execute(command: CreateOrderCommand): Promise<OrderId> {
const customer = await this.customerRepository.findById(command.customerId);
if (!customer) {
throw new CustomerNotFoundError(command.customerId);
}
const order = Order.create(customer, command.items);
await this.orderRepository.save(order);
return order.id;
}
}
Без явного : Promise<OrderId> компилятор выводит тип по реализации. Если в будущем функция случайно вернёт лишнее поле или undefined в части путей — TypeScript промолчит, а контракт расширится незаметно.
Явный тип — это граница между «что обещает функция» и «как она это делает».
async findProductById(id: ProductId): Promise<Product | undefined> { ... }
async getActiveOrders(customerId: CustomerId): Promise<Order[]> { ... }
isEligibleForDiscount(customer: Customer): boolean { ... }
Promise<Order[]> гарантирует: никогда не undefined, никогда не null — только массив (возможно пустой). Если бы аннотации не было, а реализация случайно вернула undefined на одном пути — внешний код получил бы undefined без ошибки компиляции.
Guard clause — ранний выход
NODE-17: проверки-предусловия выносятся в начало функции с ранним throw или return. Вложенные if/else для основной логики — антипаттерн.
async processPayment(
orderId: OrderId,
amount: bigint,
customerId: CustomerId,
): Promise<PaymentResult> {
const order = await this.orderRepository.findById(orderId);
if (!order) {
throw new OrderNotFoundError(orderId);
}
const customer = await this.customerRepository.findById(customerId);
if (!customer) {
throw new CustomerNotFoundError(customerId);
}
if (!customer.isActive()) {
throw new CustomerInactiveError(customerId);
}
if (order.customerId !== customerId) {
throw new OrderOwnershipError(orderId, customerId);
}
return this.sberPaymentGateway.charge(customer, amount);
}
Каждый guard занимает 3 строки и сразу заметен. Основная логика (sberPaymentGateway.charge) располагается в конце без вложенности — читается линейно сверху вниз.
Вложенный вариант хуже читается и сложнее модифицируется:
async processPayment(orderId: OrderId, amount: bigint, customerId: CustomerId) {
const order = await this.orderRepository.findById(orderId);
if (order) {
const customer = await this.customerRepository.findById(customerId);
if (customer) {
if (customer.isActive()) {
if (order.customerId === customerId) {
return this.sberPaymentGateway.charge(customer, amount);
}
}
}
}
}
Добавление нового условия в «лесенку» смещает весь блок вправо и требует следить за парными }.
Деньги и время
NODE-18: деньги — bigint в минорных единицах или Decimal-библиотека. number — запрещён для денежных вычислений.
number в JavaScript — IEEE 754 double precision. Это значит, что 0.1 + 0.2 !== 0.3. В финансовом расчёте разница в 0.000000000000001 рубля аккумулируется и становится реальной ошибкой.
const priceInKopecks = 15999n;
const quantity = 3n;
const total = priceInKopecks * quantity;
export class OrderItem {
constructor(
readonly productId: ProductId,
readonly quantity: number,
readonly priceInKopecks: bigint,
) {}
get totalInKopecks(): bigint {
return this.priceInKopecks * BigInt(this.quantity);
}
}
Для вывода в API — конвертация в строку или в number только в последний момент (на слое сериализации), не в доменном объекте:
export class OrderItemDto {
productId: string;
quantity: number;
priceInKopecks: string;
static from(item: OrderItem): OrderItemDto {
return {
productId: item.productId,
quantity: item.quantity,
priceInKopecks: item.totalInKopecks.toString(),
};
}
}
Если bigint неудобен из-за интеграций (ORM, внешний API), используется Decimal.js или big.js — но не number.
Время — UTC при хранении и передаче; конвертация в локальное время только в слое представления. В PostgreSQL — timestamptz; в TypeScript — Date или luxon.DateTime с фиксированным UTC.
Именованные предикаты для сложных условий
NODE-19: булево выражение из более чем 3 операторов &&/|| выносится в функцию с говорящим именем.
function isProductAvailableForOrder(product: Product, customer: Customer): boolean {
return (
product.isActive() &&
product.stockQuantity > 0 &&
!customer.isBlocked()
);
}
async createOrder(command: CreateOrderCommand): Promise<OrderId> {
const product = await this.productRepository.findById(command.productId);
const customer = await this.customerRepository.findById(command.customerId);
if (!isProductAvailableForOrder(product, customer)) {
throw new OrderNotAllowedError(command.productId, command.customerId);
}
...
}
Предикат с именем isProductAvailableForOrder читается как документация: сразу понятно, что именно проверяется. Инлайн-версия из 5 условий вынуждает читать каждое условие и самостоятельно понимать их совокупный смысл.
Граница в 3 оператора не строгая — важна читаемость. Три коротких && на одной строке допустимы; два сложных с вызовами методов лучше вынести в предикат.
if (
product.isActive() &&
product.stockQuantity > 0 &&
!customer.isBlocked() &&
customer.hasVerifiedEmail() &&
this.regionService.isDeliveryAvailable(customer.region)
) {
...
}
Пять условий — выносим в canCustomerOrderProduct(customer, product) или isOrderAllowed(customer, product).
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
body: any на входе контроллера | NODE-X5 | body: unknown + ZodSchema.parse(body) |
as any для «подгонки» типа | NODE-X5 | исправить модель типов |
order!.customerId (non-null assertion) | NODE-X6 | guard if (!order) или ?? |
as Order без валидации | NODE-X7 | type guard или схема-валидатор |
| Функция без аннотации возвращаемого типа | NODE-16 | явный : Promise<T> / : T |
let price = 199.99 для денег | NODE-18 | const priceInKopecks = 19999n |
new Date().toLocaleDateString() при хранении | NODE-18 | UTC toISOString() |
if (a && b && c && d && e) | NODE-19 | именованный предикат |
Вложенный if/else вместо guard clause | NODE-17 | ранний throw/return |
Куда дальше
- node/naming.md —
kebab-caseфайлов,PascalCaseклассов, глагольные методы, DI-токены. - node/imports.md — named exports, path-aliases,
import type, запрет наrequire. - node/async.md —
async/awaitвместо.then()-цепочек,Promise.all, floating promises. - node/immutability.md —
readonlyна полях,as const, spread вместо мутации аргументов. - node/tooling.md —
tsconfig.jsonstrict, ESLint strictTypeChecked, CI-прогон. - Раздел «Стандарты → Code Style» — хаб языковых биндингов: Java, Node, Python, Go.