ADR 011: TypeScript Strict Mode — Gradual Enablement
Status
Completed - 2026-03-07
Context
Ситуация до изменений
| Package | strict | strictNullChecks | noImplicitAny | strictFunctionTypes |
|---|---|---|---|---|
| backend | false | false | false | false |
| frontend | true | (implied) | (implied) | (implied) |
| admin | true | (implied) | (implied) | (implied) |
Frontend и admin работают в strict-режиме. Backend — полностью без strict. Тот же паттерн в GoBind2: backend strict=off, frontend strict=on.
Аудит: что ломает strict на backend (310 ошибок)
| Код | Кол-во | % | Суть | Источник |
|---|---|---|---|---|
| TS2769 | 121 | 39% | No overload matches | Fastify route definitions |
| TS2345 | 73 | 24% | Argument type mismatch | Service/Prisma calls |
| TS2322 | 58 | 19% | Type assignment | unknown -> T, generics |
| TS2430 | 19 | 6% | Interface extends | AuthenticatedRequest vs FastifyRequest |
| TS7006 | 13 | 4% | Implicit any | steam-bot, untyped libs |
| Other | 26 | 8% | Misc | — |
~65% ошибок (TS2769 + TS2430) — Fastify type system, не бизнес-логика.
Routes — главный источник: seasons/routes (60), admin/routes (44).
Почему strict был выключен
- Fastify plugin system генерирует сложные generic типы для route handlers.
strict: trueтребует explicit typing каждого route — 140+ ошибок из route definitions AuthenticatedRequestaugmentation конфликтует с Fastify generics в strict mode- Steam trade bot использует untyped JS SDK (
steam-tradeoffer-manager,steamcommunity)
Decision
Постепенное включение strict-флагов по отдельности, от простого к сложному.
Roadmap
| Шаг | Флаг | Реальные ошибки | Сложность | Статус |
|---|---|---|---|---|
| 1 | strictFunctionTypes: true | 5 (Fastify Logger type) | Easy | Done |
| 2 | noImplicitAny: true | 45 production + 134 test | Medium | Done |
| 3 | strictNullChecks: true | 102 (actual) | Hard | Done |
| 4 | strict: true | ~76 (mainly TS2769 Fastify) | Hard | Done |
Текущее состояние (после всех 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 FastifyBaseLoggerwebhook.routes.ts: добавлен route generic<{ Params: { steamId: string } }>— идиоматичный Fastify
noImplicitAny (45 production ошибок):
- Steam modules: создан
steam-modules.d.tsdeclaration file для untyped JS SDKs - Proper interfaces:
SchedulerStats,SpinAvailabilityResult— вместоnull as Type - Optional fields: удалены
undefinedзначения для optional interface полей - Return type annotations: explicit types на
.catch()callbacks иsafeExecutelambdas - 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 typeofcast дляthis.prisma[tableName] - Proper destructuring:
const { campaign: _, ...rest } = shortLinkвместоcampaign: undefined
strictNullChecks (102 ошибки):
Основные паттерны:
handleAnalyticsErroroverloads:shouldThrow: true→ return typenever(9 ошибок)@fastify/jwtaugmentation:user: JWTPayload | undefined(19 AuthenticatedRequest extends)- Prisma JSON fields:
null→Prisma.DbNullдля where/data (9 ошибок) JsonValue→Prisma.InputJsonValueпри записи обратно в Prisma (8 ошибок)string | null→string | undefined:?? undefined(7 ошибок)- Custom types обновлены для nullable полей (raffle, quest, banner types)
this.managerв callbacks: захват в локальную переменную для narrowingprofileControllerlazy 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 коде:
bp-award.job.ts:boost.source.type— полеtypeне существует на grammyChatBoostSource. Правильно:boost.source.sourceadmin-utm-analytics.controller.ts:campaign.clicksCount— свойство не существует наUTMStatsData. Правильно:campaign.clicksstreak-points.repository.ts:lastLoginDate: nullфильтр — поле required, null-записей невозможноuser-crud.service.ts:telegramId: bigint— Prisma schemaString, неBigIntquest.types.ts: 11 rust progress полей отсутствовали вQuestWithProgressinterface
Принципы
- Не обходить strict через
as any— если ошибка, чинить корень - Boundary casts допустимы —
unknown[]-> domain type при работе с untyped JS SDKs - Prisma JSON fields —
Prisma.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 фиксов