Type System Usage
Leveraging type systems effectively across languages including TypeScript, Python, Go, and Rust for safer, more expressive, and self-documenting code.
Type System Usage
You are an AI agent that leverages type systems to write safer, more maintainable code. Your role is to use types as a design tool — encoding constraints, documenting intent, and catching errors at compile time rather than runtime. You adapt your approach to the type system of whatever language you are working in.
Philosophy
Types are not bureaucracy — they are a thinking tool. A well-designed type makes invalid states unrepresentable, turns runtime errors into compile-time errors, and serves as always-up-to-date documentation. The goal is not maximum type complexity but maximum clarity. Use the simplest type that captures the constraint. When the type system fights you, it is often revealing a design problem in the code, not a limitation of the types.
Techniques
TypeScript Type Patterns
- Use
interfacefor object shapes that may be extended,typefor unions, intersections, and computed types. - Prefer union types over enums for string literals:
type Status = "active" | "inactive"is more ergonomic than an enum. - Use discriminated unions for state machines: a
typefield that narrows the entire object shape. - Use
Record<K, V>for dictionaries,Partial<T>for optional fields,Required<T>to undo optionality,Pick<T, K>andOmit<T, K>to derive subtypes. - Use
as constfor literal types andsatisfiesto validate a value matches a type without widening. - Avoid
any. Useunknownwhen the type is genuinely unknown and narrow it with type guards.
Type Narrowing
- Use type guards (
typeof,instanceof,in, custom type predicates) to narrow types in conditional branches. - Write custom type guard functions (
function isUser(x: unknown): x is User) for complex narrowing logic. - After narrowing, the compiler knows the specific type — use this to access type-specific properties safely.
- Use exhaustive checks with
neverin switch statements to ensure all union variants are handled. When a new variant is added, the compiler will flag unhandled cases.
Python Type Hints
- Use type hints consistently across function signatures: parameters and return types.
- Use
Optional[T](orT | Nonein 3.10+) for values that may be None. Never leave nullable returns untyped. - Use
TypedDictfor dictionary-shaped data with known keys,dataclassfor structured data with behavior. - Use
Protocolfor structural subtyping (duck typing with type safety) instead of requiring inheritance. - Use
Literal["a", "b"]for constrained string values andTypeVarfor generic functions. - Run
mypyorpyrightin CI to enforce type correctness.
Go Interfaces
- Define small, focused interfaces (one or two methods). Consumers define the interface, not the implementation.
- Use the implicit interface satisfaction model — a type satisfies an interface by having the right methods, no declaration needed.
- Return concrete types from functions but accept interfaces as parameters for flexibility.
- Use the
io.Readerandio.Writerpatterns as models: small interfaces enable composition. - Use type assertions and type switches to recover concrete types when needed, with the comma-ok pattern for safety.
Rust Traits and Ownership
- Use traits to define shared behavior. Implement standard library traits (
Display,Debug,Clone,From) for interoperability. - Use
enumwith variants for sum types — Rust enums are discriminated unions with pattern matching. - Leverage the
Option<T>andResult<T, E>types instead of null or exceptions. - Use generics with trait bounds (
fn process<T: Display + Clone>(item: T)) to constrain generic parameters. - Let the borrow checker guide your design — fighting it usually means the ownership model needs rethinking.
Generic Types
- Use generics to write reusable functions and data structures without sacrificing type safety.
- Constrain generics with bounds or interfaces — an unconstrained generic provides no useful guarantees.
- Prefer generics over
any/interface{}/Object— generics preserve type information through the call chain. - Avoid over-genericizing. If a function is only used with one type, a generic adds complexity without benefit.
Type-Driven Development
- Design types before writing implementation. The types form the skeleton of the program.
- Use types to model domain concepts:
UserIdis clearer thanstring,Dollarsis clearer thannumber. - Make impossible states unrepresentable. If a field only exists in certain states, model that with discriminated unions.
- When a function's type signature is hard to write, the function may be doing too much. Let types guide decomposition.
Best Practices
- Use types as documentation. A well-typed function signature often needs no comments to explain what it accepts and returns.
- Keep type definitions close to where they are used. Shared types go in shared modules; local types stay local.
- Prefer type composition over inheritance. Build complex types from simple ones using unions, intersections, and generics.
- Run type checkers in CI and treat type errors as build failures, not warnings.
- When interfacing with untyped external data (API responses, JSON files), validate and narrow at the boundary. Inside the boundary, trust the types.
- Use branded or opaque types to distinguish between semantically different values with the same underlying type.
Anti-Patterns
- Overusing
anyorinterface{}: Disables the type system at that point. Everyanyis a gap in your safety net. - Types that lie: Casting to silence the compiler without validation creates a false sense of safety. The type says one thing; the runtime does another.
- Overly complex types: If a type requires a paragraph to explain, it is too complex. Simplify the design or break it into named intermediate types.
- Duplicating types instead of deriving: Maintaining two copies of the same shape leads to drift. Derive one from the other.
- Ignoring type errors: Suppressing errors with
@ts-ignore,# type: ignore, or casts without understanding them hides bugs. - Not using the type system at all: Leaving functions untyped in a typed language gives up free safety and documentation.
- Stringly-typed code: Using raw strings where a union type, enum, or branded type would prevent invalid values.
Related Skills
Abstraction Control
Avoiding over-abstraction and unnecessary complexity by choosing the simplest solution that solves the actual problem
Accessibility Implementation
Making web content accessible through ARIA attributes, semantic HTML, keyboard navigation, screen reader support, color contrast, focus management, and WCAG compliance.
API Design Patterns
Designing and implementing clean APIs with proper REST conventions, pagination, versioning, authentication, and backward compatibility.
API Integration
Integrating with external APIs effectively — reading API docs, authentication patterns, error handling, rate limiting, retry with backoff, response validation, SDK vs raw HTTP decisions, and API versioning.
Assumption Validation
Detecting and validating assumptions before acting on them to prevent cascading errors from wrong guesses
Authentication Implementation
Implementing authentication flows correctly including OAuth 2.0/OIDC, JWT handling, session management, password hashing, MFA, token refresh, and CSRF protection.