TypeScript in Node Best Practices
TypeScript pays off when strict compile-time checks meet runtime validation at boundaries - not when any leaks through ORM results and request bodies.
How to Use This List
- Apply to every new service and every migrated module leaving
allowJs. - Block PRs that add
anyor@ts-ignorewithout ticket justification. - Pair with
tsc --noEmitand tests in CI - types without tests still ship bugs. - Revisit when upgrading TypeScript major versions for new strict flags.
A - Compiler and Project Setup
- Enable
strict: truefor all new projects. Add stricter flags (noUncheckedIndexedAccess) when team is ready. - Use
module/moduleResolution:NodeNexton Node 24 ESM. Matches runtime resolver. - Run
tsc --noEmitin CI on every PR. tsx dev alone does not typecheck. - Emit
dist/for production containers.CMD ["node", "dist/main.js"]- no tsx in prod image. - Pin
typescriptand@types/nodeto compatible majors. Align@types/nodewith Node 24.
B - Boundaries and Runtime
- Validate
process.envwith Zod at startup. Fail before listening on a port. - Parse HTTP bodies, queries, and params with schemas. Never
as Fooonreq.body. - Derive types with
z.infer- no duplicate interfaces. Single source of truth. - Map database entities to DTOs explicitly. Do not export ORM models to clients.
- Treat external SDK responses as
unknownuntil parsed. Even from trusted vendors.
C - Code Style and Safety
- Prefer
import typefor type-only imports. EnableverbatimModuleSyntaxon new repos. - Avoid
any- useunknown+ narrow. ESLint@typescript-eslint/no-explicit-anyas error. - Limit non-null assertions (
!) to tests. Production code uses guards or Zod. - Use discriminated unions for result types.
{ ok: true, value } | { ok: false, error }. - Do not
@ts-ignore- use@ts-expect-errorwith ticket and removal date.
D - Framework Integration
- Augment Express
Requestfor cross-cutting fields only. Per-route generics for body/params. - Use Fastify schemas or Zod type provider. One validation source for Fastify 5 routes.
- Type async handlers as
Promise<void>return. Ensure errors reach framework error hooks. - Share DTO packages - not server internals - with frontend.
@acme/api-typespattern. - NestJS DTOs: validate with pipes, not decorators alone. class-validator or Zod at boundary.
E - Migration and Maintenance
- Ratchet: no new
.jsfiles in typed directories.allowJsis temporary. - Convert leaf utilities before route files. Bottom-up migration reduces graph churn.
- Add types to
node:testsuites early. Tests lock behavior during JS → TS rename. - Review TypeScript release notes for new strict options quarterly. Opt in deliberately.
- Keep shared types package free of
node:*imports. Safe for React bundlers.
FAQs
Is any ever allowed?
Rarely - third-party escape hatches with immediate narrow wrapper. Never in domain logic.
Should I enable noUncheckedIndexedAccess?
Recommended for new code - catches arr[i] undefined. Noisy on brownfield - enable per directory.
Do I need both Zod and TypeScript?
Yes - TS for developers, Zod for runtime data crossing trust boundaries.
Can I skip declarations in apps?
Private apps can skip publishing .d.ts - libraries and shared types packages need them.
What about Prisma types?
Prisma types are DB-shaped - map to DTOs before HTTP responses.
Should tests be included in tsc?
Either include **/*.test.ts in main tsconfig or separate tsconfig.test.json - both must typecheck in CI.
How do I ban implicit any?
strict + ESLint noImplicitAny equivalent rules from typescript-eslint recommended type-checked config.
Is satisfies operator useful?
Yes - config satisfies Config validates object literals without widening - great for static config maps.
What about enum vs union?
Prefer as const + unions or Zod enums - better runtime and tree shaking than TS enum.
Does Express 5 need @types/express 5?
Align versions - mismatch causes wrong handler typings.
How strict is too strict for migration?
Use secondary tsconfig.strict.json include glob - expand monthly until main config covers all src.
Where is env validation documented?
Zod at Boundaries - canonical env and body patterns.
Related
- Zod at Boundaries - runtime validation
- TypeScript in Node Basics - starter tsconfig
- Gradual Typing Brownfield - migration playbook
- Typecheck in CI - enforcement
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.