In TypeScript, when should a shared type be declared explicitly and enforced onto the implementation, rather than derived from it with `ReturnType<typeof ...>`?
answer
- which artefact is the source of truth
- derive down, declare across
- a boundary is a promise you made
- silent reshaping versus one loud error
- named types read better in diagnostics
basics
~20 sDeclare the type wherever it is a contract others depend on: a package's public API, a cross-team boundary, a persisted or wire shape. Derive inside a module where the implementation is genuinely the source of truth, such as store state or internal factory results.
solid answer
~40 sThe question is which artefact is the source of truth. Deriving with `ReturnType<typeof createStore>` makes the *implementation* authoritative, which is right when the two can only change together — a store's state shape, a private factory's result, a fetcher feeding one module. At a boundary, the *type* should be authoritative: declare it, then check the implementation against it with an explicit return annotation or `satisfies`. That way an accidental change breaks at the function, with one clear error, instead of silently reshaping a published contract and surfacing as a dozen confusing errors in consumers. Derived types also degrade the reading experience — hovers show a resolved blob rather than a named contract — and they make emitted declaration files depend on inference. The heuristic I use: derive down, declare across.
go deeper
Understand that a derived type follows the implementation automatically, so changing the function changes the type everywhere it is used.
Explain the concrete costs of derivation — silent contract changes, structural error messages instead of named ones, inferred shapes leaking into exports — not just the convenience.
Apply the rule per boundary: derive inside a module, declare and enforce at anything a consumer depends on, and never derive from a dependency you do not control.
Own the policy and its enforcement: where the codebase's contracts are written down, how implementations are checked against them, and how derivation depth is kept from turning diagnostics into structural noise.
## Two directions of authority Every shared type in a codebase has an implicit direction. Either the implementation defines the type — you derive with `ReturnType<typeof fn>` or `Awaited<...>` and consumers follow — or the type defines the implementation, which is annotated or `satisfies`-checked against it. Both are defensible. Choosing per-boundary rather than picking one style for the whole repo is the judgment being tested. ## What derivation buys The win is real and it is why the idiom spread. There is no duplicate to keep in step, refactors propagate for free, and a whole category of drift bug — the interface that quietly stopped matching the object literal — cannot occur. For a store's state, the reducers *are* the definition; writing a parallel `RootState` interface by hand is pure liability: ```ts type RootState = ReturnType<typeof store.getState>; type AppDispatch = typeof store.dispatch; ``` ## What derivation costs **Contract changes stop being visible.** If `getUser` gains a field, every derived consumer silently accepts the new shape. That is convenient inside a module and dangerous across a package boundary, where the shape is a promise you made to someone else. A declared type turns the same edit into one error at the function — the place a reviewer can actually judge it. **Error messages get worse.** A named `interface User` appears in diagnostics as `User`. A derived alias resolves to its structure, so a mismatch prints a wall of properties. On a deep chain — `Awaited<ReturnType<typeof handler>>["data"][number]` — the reader can no longer tell what was supposed to be true. **Inference leaks internals.** Whatever the implementation happens to return becomes the contract, including fields that were incidental, and including types imported from private modules. A declared type is a deliberate projection; a derived one exports whatever fell out. **Emitted declarations depend on inference.** When a package ships `.d.ts` files, derived public types make the emitted output a function of how much the compiler could infer. Explicit annotations at exported boundaries keep that output stable and cheap to produce. ## The middle position The choice is not binary, and the strongest answer says so. You can keep the type authoritative *and* keep the implementation honest: ```ts export interface User { id: string; name: string; } // The type is the contract; the function is checked against it. export async function fetchUser(id: string): Promise<User> { const raw = await getRow(id); return { id: raw.id, name: raw.name }; } ``` An explicit return annotation costs one line and moves every failure to the definition. For object constants, `satisfies` does the same job while preserving the narrower inferred type for local use. Either way the contract is written down and the implementation must live up to it — the opposite of derivation, achieved without duplicating a shape by hand. ## A workable policy - **Derive down.** Inside a module or feature slice, where implementation and consumers ship together and no one outside can observe the shape, derive freely: store state, thunk results, factory outputs, private fetchers. - **Declare across.** At any boundary someone else depends on — a published package's exports, an API response, a persisted record, a contract between teams — declare the type and enforce it with an annotation or `satisfies`. - **Never derive from something you do not control.** `ReturnType<typeof someLibraryFn>` couples you to a shape the library never promised, and it can change in a patch release without notice. - **Cap the depth.** A chain of three or more utilities over a `typeof` query is usually a sign that a named type should have existed two steps ago. Name the intermediate. ## The cost that is *not* real Worth saying explicitly, because candidates reach for it: neither approach costs anything at runtime. Types are erased, and a derived type does not call, inspect, or retain the function it was derived from. The entire tradeoff is about change management, diagnostics, build-time behaviour and human readability — never about performance of the shipped program.
- How do you keep a declared type authoritative without duplicating the shape by hand in the implementation?Annotate the function's return type, or use `satisfies` on an object constant. Both make the declared type the contract while the implementation is checked against it — no second copy of the shape exists, and a mismatch is reported at the definition rather than at every consumer. `satisfies` additionally preserves the narrower inferred type for local use.
- What is wrong with deriving a type from a third-party function's return value?You couple your code to a shape the library never promised. The function's inferred return type is an implementation detail that can change in a patch release, and the failure surfaces in your consumers rather than at the dependency edge. Declare your own type at that seam and adapt to it, so a library change produces one localised error.
- Does the derive-versus-declare choice have any runtime consequence?None. Types are erased, so both approaches emit identical JavaScript, and a derived type never calls or inspects the function it came from. The tradeoff is entirely about change management, diagnostic quality, declaration-file emit and readability — anyone who argues the point on runtime performance has misunderstood the type layer.
saying these in an interview costs you the question
- Claims derived types cost something at runtime
- Treats derivation as always better because it avoids duplication
- Ignores who consumes the type when choosing
- Derives public API types from third-party return values
- Cannot name a downside of implementation-as-source-of-truth