Types must model the domain and fail early. Compile-time does not replace validation at the boundary. Form UX: better-forms.
Rules
- Treat
strictas the baseline. Do not turn off flags to “pass the build”. - Forbid new
any. Preferunknown+ narrowing. - Avoid blind
as; use type guards, predicates, and schemas. - Model nullability explicitly (
T | null), not with magic sentinels. - Keep domain types in dedicated modules; do not spread duplicate interfaces.
Modeling
- Use union types and discriminated unions for states.
- Prefer
Readonly/as constfor immutable data. - Generics: constrain with
extends; avoid decorative generics. - Extract types from values (
typeof,ReturnType,Awaited) instead of duplicating. - For external APIs, define request/response types and equivalent Zod/Valibot schemas.
Runtime
- Validate at the boundary: HTTP, forms, env, webhooks, localStorage.
- Inference:
z.infer<typeof Schema>as a single source. - Typed errors: result types or discriminable error classes.
- Env: parse with a schema at boot; fail if invalid.
Safe refactoring
- Introduce types at the boundary.
- Push
unknowninward until narrowing. - Remove casts gradually.
- Run
tsc --noEmit/ project typecheck after changes.
Anti-patterns
any,as any,@ts-ignorewithout a short, local justification- Giant interfaces with too many optional fields
- Fragile numeric enums when a union of strings is enough
- Typing only on the frontend while the backend returns
any - Lying to the compiler instead of fixing the model
Completion criteria
- No unjustified
anyin the areas touched - Boundaries validated at runtime
- Discriminated unions for complex states
- Project typecheck passing
- Types readable and aligned with the domain