TypeScript Guidelines
Use this skill for project-wide TypeScript conventions before loading narrower skills such as arktype, typebox, testing, or method-shorthand-jsdoc.
When To Apply This Skill
Use this skill when you need to:
- Write or refactor TypeScript with Epicenter naming and style conventions.
- Decide whether to derive, import, or declare a type.
- Review type ownership, copied shapes, factory return types, brands, casts, and generic names.
- Choose clear value-mapping and control-flow patterns for unions and discriminated values.
- Organize type tests, runtime schemas, or factory-focused refactors.
Core Rules
- Try to derive or import a type before declaring a new named type. New named types must earn their place as a real contract, protocol vocabulary, discriminated result union, capability port, or multi-implementation shape.
- Treat local shape copies as boundary smells. Prefer the owning runtime type, schema inference, factory return type, function signature, or a caller-owned capability function.
- Use
type, notinterface. - Use
readonlyonly for arrays and maps, unless matching an upstream type exactly. - Treat acronyms as normal words in camelCase:
parseUrl,defineKv,readJson,customerId. - Use
.jsextensions in relative imports. Do not use extensionless or.tsrelative imports. - Export symbols at their declarations. Reserve
export { ... } from ...for barrel files. - Prefer factory functions over classes. Let closure position communicate private vs public API.
- Use descriptive generic names with a
Tprefix, such asTSchema,TDefs, andTKey. - Destructure options in the function signature when the object is a configuration bag. Keep a named value only when it is the domain object being transformed or forwarded.
- Let TypeScript infer private and inner return types. Annotate exported APIs only when useful for clarity or to break circular inference.
- If an exported type is exactly the object returned by a
create*,attach*,open*, or similar factory, derive it from the implementation withReturnType<typeof createThing>. Put the exported output alias immediately after the factory. Keep input, config, data, protocol, and multi-implementation contract types above the factory. - Move consumer-facing JSDoc onto the returned object members. Add concrete member annotations inside the returned object when they preserve IntelliSense, narrow an implementation detail, or keep a public method/property surface stable.
- For curried factories, derive from the inner return, such as
ReturnType<ReturnType<typeof createThing>>. For generic factories, instantiatetypeofwhen needed, such asReturnType<typeof openThing<TActions>>. - Preserve intentional readonly public surfaces with getters when deriving from an object literal. Do not expose writable internal state just because the concrete implementation happens to store it that way.
- Use a
Symbolbrand when identity means a specific factory output, not a coincidental shape probe. - Avoid
as any. Useunknown, validation, brands, or narrower helpers instead. - Prefer optional chaining over
inchecks or truthiness when checking optional properties. - Use
is,has, orcanprefixes for booleans that answer a question. - Prefer
switchoverif/elsefor repeated equality comparisons against the same value. Usedefault: value satisfies neverfor exhaustiveness when needed. - Prefer
Recordlookup tables over nested ternaries for finite value mappings. - Compose typed errors bottom-up. Do not filter a broad upstream error union at the boundary.
- Question silent fallbacks that hide invalid state. Preserve round-trip invariants when parsing and serializing.
Go-to-Definition Awareness
When organizing types and exports, always consider Go-to-Definition. A developer pressing Go-to-Def from a call site should land as close as possible to the actual source of truth. If a design choice forces an extra navigation hop, the choice has to earn it elsewhere (e.g., a real validation boundary, a published contract, or a multi-implementation port).
Concrete regressions to watch for:
- Destructure-re-export of a module-level object:
const stub = { fn, gn } satisfies T; export const { fn, gn } = stub;lands Go-to-Def on the destructuring line, not the real definition. Prefer per-exportsatisfiesor a directexport const fn = ... satisfies T['fn']. typeof Realannotation oversatisfies:export const fn: typeof Real = unreachablehides the underlying value's identity from navigation.export const fn = unreachable satisfies typeof Realkeeps the value as the source of truth.- Re-export chains in non-barrel files:
export { X } from './alias'outsideindex.tscosts an extra hop with nothing to show for it. Reserveexport { ... } from ...for barrels; export at the declaration everywhere else. - Adapter / proxy / wrapper with no behavior change: a
fromXtranslator or thin passthrough makes Go-to-Def land on the wrapper. Widen the underlying factory's return shape instead (seefactory-function-composition"collapsed adapter" rule). - Manual return type annotation duplicating zone 4: annotating a factory with a hand-written interface diverts Go-to-Def to the alias. Let the factory return its concrete object, then put the exported alias directly after it as
export type Thing = ReturnType<typeof createThing>. This keeps navigation on the returned members and lets their JSDoc own the public documentation. Seemethod-shorthand-jsdoc. - Noisy
satisfiesgeneric lists: if a return object should prove it extends a generic contract butsatisfies Contract<A, B, C> & Extrasforces callers to restate inferred table, action, or runtime types, prefer a constrained identity helper owned by the contract module. Example:return defineWorkspace({ ...workspace, ...runtime })where the helper acceptsTWorkspace extends Workspace<...>and returnsTWorkspace. This keeps the call site readable, preserves the exact inferred return type, and leaves Go-to-Def on the real object members. - When not to add
defineX: do not wrap a simplesatisfiescheck just to give it a helper name. If the contract has no required type arguments, or its generics have defaults that makesatisfies Contractreadable, prefersatisfies. The helper only earns the extra name when it removes generic noise the reader would otherwise have to carry.
For broader public-shape decisions that affect navigation across packages, see cohesive-clean-breaks.
Reference Map
- Project conventions: detailed examples for derived types, local shape copies, imports, barrels, factories, generics, destructuring, and factory return types.
- Type safety and control flow: identity brands, casts, optional properties, boolean naming, switches, record lookups, error composition, fallback smells, and round-trip invariants.
- Type organization:
types.tslocation, co-location rules, inline-vs-extract hop test, options and ID naming. - Factory patterns: factory-focused refactors, parameter destructuring, and coupled state extraction.
- Runtime schema patterns: arktype, branded IDs, optional property syntax, and workspace table IDs.
- Testing patterns: inline single-use setup and source-shadowing tests.
- Advanced TypeScript features: iterator helpers and const generic array inference.