Overview
Enforce readable, boundary-aware TypeScript in strict codebases. Prefer code that a reviewer can parse quickly over clever inline type composition. If the repository already documents a stronger local convention, follow the repository before applying this skill mechanically.
Working Mode
- Identify whether the code is a boundary type, an internal helper type, or UI-local state.
- Optimize first for semantic clarity in signatures and exported APIs.
- Use utility types as construction tools, not as the main surface a human reads.
- Prefer lint-enforceable rules over taste-based advice.
- If the repository already has a stronger local rule, follow the repository.
Core Rules
Prefer named types over inline type algebra
- Extract
Pick,Omit, intersections, mapped types, and long unions into a namedtypeorinterfacebefore using them in parameters, returns, props, or exports. - Do not leave dense expressions like
Pick<Foo, "a" | "b" | "c"> & Barinline in a function signature unless the expression is trivially short. - Name the type for the business meaning, not the mechanism. Prefer
EnvironmentSessionRowoverPickedEnvironmentFields.
Use interface for object contracts and type for composition
- Prefer
interfacefor exported object shapes that are meant to be implemented, extended, or merged naturally. - Prefer
typefor unions, tuples, conditional types, mapped types, utility-type compositions, branded types, and extracted aliases. - Do not force
interfacewhere the shape is fundamentally a union or type-level transform.
Keep boundary types explicit
- Write explicit DTO or view-model shapes when a type crosses a service boundary, UI boundary, persistence boundary, or package boundary and the fields are stable.
- Use
PickorOmitonly when the derived shape is tightly coupled to the source type and that coupling is useful. - When a derived type starts accumulating exceptions, stop composing and write the object shape explicitly.
Treat null as a contract, not a default
- Use
nullonly when an external system, schema, protocol, or domain state explicitly distinguishes empty from omitted. - Prefer
undefined, optional properties, or absent fields for internal optionality unless the contract already usesnull. - Keep one absence model per boundary whenever possible. Do not mix optional,
undefined, andnullwithout a concrete reason.
Avoid anonymous structural duplication
- Reuse shared entities and exported subpath contracts before inventing app-local duplicates.
- If a local shape differs for a real reason, give it a new semantic name and document the boundary.
- Do not import broad root barrels when the repository provides stable subpath exports.
Keep React types boring
- Type props and state with named contracts when the shape is reused, edited, or non-trivial.
- Do not mirror props into state only to satisfy typing convenience.
- Avoid effect-driven type workarounds; derive values directly when possible.
- Keep component-local helper types local unless they cross module boundaries.
Decision Rules
Pick and Omit
Use them when:
- the source type is canonical
- the subset is small and obviously coupled to the source
- extraction improves reuse or prevents drift
Stop using them when:
- the field list is long enough to hide intent
- the resulting type appears in a public signature
- the subset represents a new domain concept
- multiple utility types are chained together to express one business type
Preferred rewrite:
type EnvironmentSessionRow = Pick<
PipelineUserEnvironmentRow,
| "pipelineId"
| "userId"
| "type"
| "environmentId"
| "deviceCode"
| "metadata"
| "hostConnectionId"
| "hostAuthMode"
| "hostAuthTokenId"
| "hostAuthSessionId"
>;
function mapEnvironment(row: EnvironmentSessionRow) {
// ...
}
Write the shape explicitly instead when the extracted alias still hides the business meaning.
null vs undefined
Use T | null when:
- a database column is nullable
- JSON or RPC payloads intentionally send
null - the domain has a real "present but empty" state
Use ? or undefined when:
- a caller may omit the field
- the value is optional only during local computation
- the code is modeling partial construction, config, or component state
Review Checklist
Check these in order:
- Can a reviewer understand the exported type or function signature without mentally expanding utility types?
- Does the type name communicate domain meaning instead of implementation mechanics?
- Is
nulljustified by a real boundary contract? - Is a shared contract already available?
- Can the proposed rule be enforced by ESLint, TypeScript, or code review guidance?
Implementation Notes
- Keep rule text short and directive.
- Prefer examples that show the before/after of a readability improvement.
- When proposing repo-wide rules, pair style guidance with lint rules or review heuristics from
references/external-guides.md. - If a repository already documents a different convention, record the exception instead of overwriting it silently.
References
- Read
references/external-guides.mdwhen you need upstream guidance or links to published style guides.