name: typescript-principles description: TypeScript design principles covering type safety, generics, async patterns, runtime validation (Zod/Valibot), and avoidance of any/as escape hatches. Apply when reading, writing, or reviewing .ts or .tsx files. user-invocable: false paths: - "**/*.{ts,tsx}"
TypeScript Design Principles
Type Safety
- No
any-- useunknownwith type guards or narrowing (Escape Hatches below for rare exceptions) - No
as Tassertions -- use type guards,z.parse(), orsatisfies - No non-null assertions (
!) without justification as constfor literal types,satisfiesfor compile-time validation without wideningcatch: narrowunknownwithinstanceof Errorbefore property access- Zod/Valibot validation at system boundaries (API responses, user input, env vars)
- Prefer ECMAScript features over TS-only features: union types over
enum, modules overnamespace - No object wrapper types --
stringnotString,numbernotNumber,booleannotBoolean
Type Design
- Types MUST represent only valid states -- invalid combinations MUST be unrepresentable
- Prefer unions of interfaces over interfaces with union properties
- Push
null/undefinedto the perimeter -- inner functions receive validated data nullfor intentionally absent values from external sources (DB, API);undefinedfor optional/not-provided -- do not mix- Do NOT embed
| nullor| undefinedin reusable type aliases -- callers compose nullability at the usage site - Discriminated unions with exhaustive
switch+neverdefault for completeness - Prefer string literal unions over bare
string(e.g.,"pending" | "active") - Structural typing: two types with the same shape are interchangeable -- use branded types (
UserIdvsOrderId) to make invalid substitutions a compile error - Name types in domain language --
Temperature, notNumberWithUnit - Avoid optional properties when a value is always present in a given state -- use separate types per state
- Limit repeated parameters of the same type -- use named objects instead
- Be liberal in what you accept, strict in what you produce -- input params accept
readonly T[], return types are concrete - DRY types: derive with
Pick,Omit,Partial,ReturnType,typeof-- one source of truth - Prefer
Record<K, V>or mapped types over index signatures ({[key: string]: T}) - Prefer imprecise types to inaccurate types -- a
stringparam accepting some invalid values is better than a complex template literal that rejects valid inputs
Type Inference & Narrowing
- Let TypeScript infer when the type is obvious -- annotate return types and public API boundaries
satisfiesto verify inferred types match an expected shape without widening- Narrow with
in,instanceof, discriminant checks -- avoid user-defined type guards unless necessary - After narrowing, use the narrowed variable consistently -- do NOT fall back to the original unnarrowed alias
- Build objects all at once instead of incrementally adding properties
readonlyfor data that MUST NOT mutate (props, config, store state, function params that should not be modified)- Prefer
typefor unions, intersections, mapped types;interfacefor extensible object shapes and better compiler caching - Do NOT reuse a variable for different types -- create a new
constwith a descriptive name - Extracting a callback to a named function can lose generic type context -- inline when types depend on surrounding generics
- Use functional constructs (
map,filter,flatMap) over imperative loops -- types flow through without manual annotation
Generics
- Think of generics as functions between types -- input type -> output type
- Avoid unnecessary type parameters -- if a param appears only once, remove it
- Constrain type parameters with
extendswhen possible (e.g.,<T extends HTMLElement>) -- prevents unintended inference and improves internal safety - Prefer conditional types over function overloads when the output type is a pure function of the input type; use overloads when call-site narrowing or runtime dispatch is the primary concern
Record<K, V>to enforce exhaustive handling of a union -- compiler errors when a new variant is added- Use optional
neverproperties to model exclusive-or types ({ a: string; b?: never } | { a?: never; b: number }) - Write type-level tests for complex generics -- use
expectTypeortsdto verify type behavior
Async & Concurrency
- All
asyncfunctions MUST have error handling - No floating promises (missing
awaitorvoid) - Sequential
await->Promise.all/Promise.allSettledwhen independent - Accept
AbortSignalfor fetch or long-running I/O
Imports & Security
- No circular dependencies or barrel file re-export bloat
- Use
import type/export typefor type-only imports -- prevents circular reference issues and improves build performance - No XSS via innerHTML -- sanitize user input
- Watch for prototype pollution in object spread and
Object.assign
Escape Hatches
When any or assertions are unavoidable:
- Use the narrowest possible scope --
anyin a local variable, not a function signature - Prefer precise variants:
unknown[]overany[],Record<string, unknown>overany,(...args: unknown[]) => unknownoverFunction - Hide unsafe assertions inside well-typed wrapper functions -- callers see a safe API, unsafety is contained and documented
Runtime Patterns
Object.keys()returnsstring[], not(keyof T)[]-- wrap in a typed helper or useMap<K, V>when key types matterObject.entries()values are typed but keys arestring-- same caveat- Covariant array trap:
Dog[]assignable toAnimal[]can cause runtime errors via mutation -- acceptreadonly Animal[]at API boundaries to prevent callers from pushing incompatible subtypes
Module & Library Design
- Export all types that appear in public API signatures -- consumers MUST NOT need to extract them
- Put
@types/*indevDependencies, notdependencies - Mirror types (redefine minimal shapes) to avoid heavy transitive dependency chains
Compiler Performance
- Prefer interfaces over intersections (
A & B) for object types -- interfaces are cached, intersections are recomputed - Avoid deeply nested conditional types -- flatten or use codegen for complex type mappings
- Use
@ts-expect-errorover@ts-ignore-- the former errors when the suppression becomes unnecessary