skip to content

In a TypeScript codebase you can either derive your internal types from external payload types with key-remapping mapped types, or declare them by hand. How do you decide, and what does the derived approach cost a team?

level: principalimportance: nice to knowfreq 16%

answer

  1. is it a function of that shape, permanently?
  2. one source of truth versus two
  3. computed names are not searchable
  4. coupling of the whole field set
  5. derive at the boundary, declare the model

basics

~20 s

Derive when the two shapes must stay in lockstep and the external type is genuinely the source of truth. Declare by hand when the internal model should evolve independently. Derivation kills drift but produces key names that no code search can find.

solid answer

~50 s

The decision is about ownership, not elegance. Derivation with a key-remapping mapped type says "this shape is a mechanical function of that one" — every upstream change propagates for free, and mismatches surface as build errors instead of `undefined`s. That is right for a boundary DTO you do not own. It is wrong for a domain model, because deriving welds your vocabulary and your field set to somebody else's release schedule: a new payload field silently becomes part of your internal type, and an upstream rename quietly renames your keys. The concrete team cost is discoverability. Generated key names appear in no source file, so grep finds nothing, IDE rename cannot reach them, go-to-definition lands on a formula, and errors are phrased in terms of the mapped type. My default: derive at the boundary, hand-declare the model, and let a mapper's signature connect them so drift is still a compile error.

go deeper

for a junior

Know the basic tradeoff: a derived type updates itself when its source changes, and a hand-written one does not. You are not expected to pick a codebase-wide policy yet.

for a middle

Be able to argue both sides on one concrete type — what drift the derived form prevents, and what a reader loses when the field names appear in no source file.

for a senior

Show the design that gets both properties: a hand-declared model plus a mapper whose signature ties it to the payload type, so a change upstream still breaks the build.

for a principal

Own the policy and its second-order effects — navigation, error-message quality, who reviews a new field, and the rule that derivation flows downstream from shapes you do not control, never upstream.

## What derivation actually promises Writing `type Domain = { [K in keyof Wire as Names[K]]: Wire[K] }` makes a claim with real force: this type is not an independent design, it is a *function* of `Wire`. Recompute the input and the output changes. That is enormously valuable where the two shapes are genuinely required to agree, because it converts an entire class of silent drift into a compile error. Hand-written twins do the opposite: they agree on the day they are written and diverge quietly forever after. So the first question is never "is this clever" but "is this shape genuinely a function of that one, permanently?" If yes, derive. If the answer is "today it happens to match", declare it by hand and let a mapper's signature check the correspondence. ## The costs, in the order a team feels them **Names that do not exist in the source.** This is the largest and most underrated. If `userId` is produced by an `as` clause, then searching the repository for `userId` finds usages but not the declaration, and an IDE rename cannot rewrite what was never written. New engineers grep, find nothing, and conclude the field is untyped. On a codebase with a handful of derived aliases this is a shrug; across a whole domain layer it is a permanent tax on navigation. **Error messages inherit the formula.** A mismatch is reported against the computed type, and the further the derivation chain goes — remap, then filter, then intersect — the further the message drifts from anything a reader typed. The team's ability to self-serve on type errors degrades roughly with the depth of the chain. **Coupling of the field *set*, not just the names.** A homomorphic derivation takes everything. When the upstream team adds three fields for their own reasons, your internal type grows three fields you never reviewed. That is fine for a DTO whose whole job is to mirror the wire, and corrosive for a domain model, which should represent your business, not their schema version. **Direction matters.** Deriving the wire type *from* your domain model is worse than the reverse: it asserts control over an interface you do not own, and the compiler will cheerfully agree with you right up until the API changes. Derive downstream, never upstream. ## Where declaration wins Hand-declared types are searchable, hoverable, documentable with real doc comments per field, and reviewable in a diff. They are also the only honest way to express a model that is *supposed* to differ from the payload: fields you deliberately drop, names chosen for your domain rather than theirs, invariants encoded as narrower types than the wire admits. If a reviewer would want to discuss the addition of a field, that field should be typed by hand, because derivation removes the review. ## The arrangement I would defend Three layers, each with the right technique. The **wire type** is declared explicitly and matched to the API contract, ideally generated from the provider's schema if one exists. The **boundary shape** may be derived — filtered, remapped, narrowed — because its whole purpose is to be a function of the wire. The **domain model** is hand-declared. A mapper function sits between the last two with both types in its signature; that signature is what makes drift a build failure without welding the model to the payload. You get the error-on-change property that made derivation attractive, and you keep the model reviewable. ## How to keep derived types tolerable If derivation is right for a case, contain it. Keep the remapping in one small module rather than inline at usage sites. Give the intermediate results named aliases so hovers and error messages have something readable to print. Keep the chain shallow — each additional transformation multiplies the distance between the message and the source. And write a couple of type-level assertions that pin the expected result, so a reader can see the resolved shape spelled out somewhere in the repository even though the compiler computed it. ## The signal to watch for The honest test is a maintenance one: when the upstream shape changes, do you *want* your type to change automatically? For a translation layer, yes — that is the entire point. For the vocabulary your product is written in, no; you want a person to decide. Answer that question first and the derived-versus-declared choice stops being a matter of taste.

  • What is the middle ground between deriving everything and declaring everything?
    Derive the boundary shape, declare the domain model, and put a mapper between them whose signature names both types. The compiler then checks the correspondence on every upstream change, so drift is still a build error, while the model stays hand-written, searchable and reviewable.
  • Which direction of derivation is safer — deriving the payload type from your model, or your model from the payload?
    Downstream only. Deriving your model from the payload states a dependency that genuinely exists. Deriving the payload from your model asserts control over an interface you do not own; the compiler will agree with the claim and the API will not, which is the worst possible combination.
  • How do you keep a derived type from degrading a team's error messages?
    Contain it: keep the derivation in one module, give intermediate results named aliases so hovers and diagnostics print something a reader typed, keep the chain shallow, and pin the expected resolved shape with type-level assertions so the concrete result exists somewhere in the repository.

saying these in an interview costs you the question

  • Treats derivation as always better because it removes duplication
  • Ignores that generated key names cannot be searched or renamed
  • Derives the external payload type from the internal model
  • Assumes upstream field additions are always safe to inherit
  • Judges the choice on elegance rather than on ownership

context