Опирается на правила: NODE-15NODE-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-X5body: unknown + ZodSchema.parse(body)
as any для «подгонки» типаNODE-X5исправить модель типов
order!.customerId (non-null assertion)NODE-X6guard if (!order) или ??
as Order без валидацииNODE-X7type guard или схема-валидатор
Функция без аннотации возвращаемого типаNODE-16явный : Promise<T> / : T
let price = 199.99 для денегNODE-18const priceInKopecks = 19999n
new Date().toLocaleDateString() при храненииNODE-18UTC toISOString()
if (a && b && c && d && e)NODE-19именованный предикат
Вложенный if/else вместо guard clauseNODE-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.json strict, ESLint strictTypeChecked, CI-прогон.
  • Раздел «Стандарты → Code Style» — хаб языковых биндингов: Java, Node, Python, Go.