Skip to main content

ADR 011: TypeScript Strict Mode — Gradual Enablement

Status

Completed - 2026-03-07

Context

Ситуация до изменений

PackagestrictstrictNullChecksnoImplicitAnystrictFunctionTypes
backendfalsefalsefalsefalse
frontendtrue(implied)(implied)(implied)
admintrue(implied)(implied)(implied)

Frontend и admin работают в strict-режиме. Backend — полностью без strict. Тот же паттерн в GoBind2: backend strict=off, frontend strict=on.

Аудит: что ломает strict на backend (310 ошибок)

КодКол-во%СутьИсточник
TS276912139%No overload matchesFastify route definitions
TS23457324%Argument type mismatchService/Prisma calls
TS23225819%Type assignmentunknown -> T, generics
TS2430196%Interface extendsAuthenticatedRequest vs FastifyRequest
TS7006134%Implicit anysteam-bot, untyped libs
Other268%Misc

~65% ошибок (TS2769 + TS2430) — Fastify type system, не бизнес-логика. Routes — главный источник: seasons/routes (60), admin/routes (44).

Почему strict был выключен

  1. Fastify plugin system генерирует сложные generic типы для route handlers. strict: true требует explicit typing каждого route — 140+ ошибок из route definitions
  2. AuthenticatedRequest augmentation конфликтует с Fastify generics в strict mode
  3. Steam trade bot использует untyped JS SDK (steam-tradeoffer-manager, steamcommunity)

Decision

Постепенное включение strict-флагов по отдельности, от простого к сложному.

Roadmap

ШагФлагРеальные ошибкиСложностьСтатус
1strictFunctionTypes: true5 (Fastify Logger type)EasyDone
2noImplicitAny: true45 production + 134 testMediumDone
3strictNullChecks: true102 (actual)HardDone
4strict: true~76 (mainly TS2769 Fastify)HardDone

Текущее состояние (после всех 4 шагов)

backend/tsconfig.json:
strict: true // Step 4 — все флаги включены
strictPropertyInitialization: false // явно выключен (Fastify plugin pattern)

Test files (*.test.ts, src/tests/) excluded from tsconfig.json (build/type-check). ESLint uses tsconfig.eslint.json which includes test files.

Что было сделано

strictFunctionTypes (5 ошибок):

  • server.ts: pinoLogger as FastifyBaseLogger — корректный cast, pino Logger superset FastifyBaseLogger
  • webhook.routes.ts: добавлен route generic <{ Params: { steamId: string } }> — идиоматичный Fastify

noImplicitAny (45 production ошибок):

  • Steam modules: создан steam-modules.d.ts declaration file для untyped JS SDKs
  • Proper interfaces: SchedulerStats, SpinAvailabilityResult — вместо null as Type
  • Optional fields: удалены undefined значения для optional interface полей
  • Return type annotations: explicit types на .catch() callbacks и safeExecute lambdas
  • Bug fixes found by strict:
    • boost.source.type -> boost.source.source (grammy API mismatch)
    • campaign.clicksCount -> campaign.clicks (wrong property name)
  • Dynamic Prisma access: keyof typeof cast для this.prisma[tableName]
  • Proper destructuring: const { campaign: _, ...rest } = shortLink вместо campaign: undefined

strictNullChecks (102 ошибки):

Основные паттерны:

  • handleAnalyticsError overloads: shouldThrow: true → return type never (9 ошибок)
  • @fastify/jwt augmentation: user: JWTPayload | undefined (19 AuthenticatedRequest extends)
  • Prisma JSON fields: nullPrisma.DbNull для where/data (9 ошибок)
  • JsonValuePrisma.InputJsonValue при записи обратно в Prisma (8 ошибок)
  • string | nullstring | undefined: ?? undefined (7 ошибок)
  • Custom types обновлены для nullable полей (raffle, quest, banner types)
  • this.manager в callbacks: захват в локальную переменную для narrowing
  • profileController lazy init → прямая инициализация при регистрации плагина

Найденные проблемы в production коде:

  • lastLoginDate: null фильтр — поле required с @default(now()), null-записей не бывает
  • telegramId: bigint в mapSeasonReferrals — должен быть string (schema: String)
  • QuestWithProgress — отсутствовали 11 rust progress полей (добавлялись через as)

Step 4 — strict: true:

  • TS2769 ошибки в webhook routes и остальные ~76 фиксов — все решены
  • Итоговый tsconfig: strict: true, strictPropertyInitialization: false

Найденные баги

Strict mode обнаружил 5 реальных проблем в production коде:

  1. bp-award.job.ts: boost.source.type — поле type не существует на grammy ChatBoostSource. Правильно: boost.source.source
  2. admin-utm-analytics.controller.ts: campaign.clicksCount — свойство не существует на UTMStatsData. Правильно: campaign.clicks
  3. streak-points.repository.ts: lastLoginDate: null фильтр — поле required, null-записей невозможно
  4. user-crud.service.ts: telegramId: bigint — Prisma schema String, не BigInt
  5. quest.types.ts: 11 rust progress полей отсутствовали в QuestWithProgress interface

Принципы

  • Не обходить strict через as any — если ошибка, чинить корень
  • Boundary casts допустимыunknown[] -> domain type при работе с untyped JS SDKs
  • Prisma JSON fieldsPrisma.DbNull для SQL NULL, Prisma.InputJsonValue для write operations

Consequences

Positive

  • Пять проблем найдены и исправлены ещё до попадания в production issues
  • Steam SDK теперь имеет type declarations — IDE подсказки работают
  • Implicit any больше не скрывает ошибки в новом коде
  • Null safety: TS предотвращает Cannot read property of null/undefined
  • Full strict mode включён — максимальная type safety

Negative

  • Тест-файлы исключены из type-check (проверяются через Vitest)
  • Нужен tsconfig.eslint.json для ESLint совместимости
  • strictPropertyInitialization: false — необходим для Fastify plugin pattern

Risks

  • Некоторые обновления Fastify могут потребовать дополнительных type фиксов