When to use
- Creating reusable type utilities.
- Building typed libraries, API clients, configs, or state helpers.
- Modeling complex type relationships.
- Replacing unsafe
anyor duplicated runtime/schema types. - Debugging inference, narrowing, or generic constraints.
Goal
Use advanced types to improve safety without making code unreadable. Prefer simple types until complexity pays for itself.
Rules
- Use
unknowninstead ofanywhen input is not trusted. - Let TypeScript infer simple types.
- Add generics only when a type must connect multiple positions.
- Prefer unions and discriminated unions for variants.
- Prefer
interfacefor object shapes. - Use
typefor unions, conditionals, mapped types, and aliases. - Avoid deeply nested conditional or recursive types.
- Add type tests for reusable utilities when the project supports them.
Core Tools
- Generics: reuse logic across related types.
- Conditional types: choose a type from a type condition.
- Mapped types: transform object properties.
- Template literal types: constrain string patterns.
- Utility types: use
Pick,Omit,Partial,Required,Readonly,Record. infer: extract inner types from arrays, promises, functions, or objects.- Type guards: narrow
unknownat runtime. - Assertion functions: throw when runtime validation fails.
- Branded types: distinguish validated strings or IDs.
Generic Rule
If a type parameter appears only once, it is probably unnecessary.
Useful Patterns
type StrictOmit<T, K extends keyof T> = Omit<T, K>;
type ElementType<T> = T extends (infer U)[] ? U : never;
type UserId = string & { readonly __brand: "UserId" };
type User = z.infer<typeof userSchema>;
Avoid
anyas a shortcut.- Type assertions when a guard is possible.
- Overly clever types that hide intent.
- Unbounded generic parameters.
- Duplicating types already available from schema or API definitions.
- Recursive types that slow the compiler.
Output
- Type approach.
- Simplest type shape.
- Generic constraints if needed.
- Runtime validation boundary when input is unknown.