When is deriving an API response type with `Omit` from a database entity type the wrong call in TypeScript, and what would you do instead?
answer
- which direction does change flow
- opt-out versus opt-in exposure
- the compiler never fires on a rename
- derive inside, declare across
- assert compatibility, do not inherit it
basics
~20 sDeriving is wrong when the two types are separate contracts that merely look alike. Omit couples your wire format to storage, fails open when a field is renamed, and never errors on a stale key — so a public contract is better declared explicitly and checked against the entity.
solid answer
~50 sDerivation is right when the response genuinely is the entity seen through a smaller window, and wrong when the two evolve for different reasons. An entity changes because storage changes; a public response changes because consumers negotiate it. Coupling them with `Omit` means every column added to the entity is added to the wire by default — the contract becomes opt-out instead of opt-in, and since `Omit` accepts keys that do not exist, a renamed sensitive column silently reappears in the response type with no error. My default for outward-facing contracts is an explicitly declared type, with a compile-time assertion that it stays compatible with the entity rather than derived from it. For internal, same-module shapes — component props, form models, patch payloads — I derive freely, because there the coupling is the point and drift is the risk I actually want to catch.
go deeper
Know that Omit can build a response type from an entity, and that doing so ties the two together — a change to the entity changes the response. Recognising the coupling is enough at this level.
Explain the mechanics of the coupling: what a new entity field does to a derived type, and why Omit reports no error when a removed key no longer exists.
Argue the failure mode concretely — exposure becomes opt-out, a rename silently re-adds a field, and the diff that causes it does not touch the response file. Show the StrictOmit or assertion pattern you would actually use.
Own the boundary rule and its cost. Say which shapes you derive and which you declare, how you enforce compatibility without inheritance, and be honest that the type layer never enforces anything at runtime — the serializer still needs its own allow-list.
## The question behind the question `Omit<UserEntity, "passwordHash">` looks like the DRY answer: one source of truth, no duplication, the response type updates itself. The judgment call is whether *self-updating* is a feature or a hazard, and that depends entirely on which direction you want change to flow. ## Two kinds of relationship Deriving expresses a claim: *this type is that type, minus a window*. That claim is true for a lot of shapes and false for a lot of others. It is true for a form model over a domain object, for component props that are a subset of a parent's state, for a PATCH payload that is a partial view of a record. These live in the same module, change together, and are meant to move in lockstep. Here derivation is not just acceptable — it is the thing that catches drift, because adding a field to the source immediately surfaces everywhere the subset is consumed. It is false for a published API response, an event payload other services consume, or anything that crosses a team boundary. Those change because a consumer negotiated a change, not because someone added a column. Deriving them makes storage decisions into contract decisions, which is precisely the coupling the boundary exists to prevent. ## The specific failure modes **Opt-out instead of opt-in.** With `Omit`, the default for a new entity field is *exposed*. Someone adds `internalRiskScore` to the entity for an analytics job; the response type grows a field, the serializer ships it, and no reviewer sees anything unusual in the diff — the response type file was not touched. With an explicit type, the default is *hidden*, and exposing a field requires an edit where a reviewer will see it. **Silent staleness.** `Omit`'s key parameter is constrained to `keyof any`, not `keyof T`, so removing a key that no longer exists is not an error. Rename `passwordHash` to `passwordDigest` and `Omit<UserEntity, "passwordHash">` still compiles — and now includes the digest. The one place you most wanted a compiler error is the one place it will never fire. **Readability at the boundary.** A consumer of your API reading `Omit<Pick<UserEntity, ...>, ...>` cannot see the contract; they have to evaluate it. Contracts are read far more often than they are written, and an explicit interface is documentation that cannot go out of date relative to itself. ## What to do instead Declare the outward type explicitly, then assert compatibility rather than derive it. The assertion keeps the two honest without making one define the other: ```ts interface UserEntity { id: string; email: string; displayName: string; passwordHash: string; } interface UserResponse { id: string; email: string; displayName: string; } // Fails to compile if a response field stops existing on the entity // or changes type; adding an entity field changes nothing here. type AssertCompatible = UserResponse extends Pick<UserEntity, keyof UserResponse> ? true : never; const _check: AssertCompatible = true; ``` The direction matters: the check flows *response to entity*, so removing or retyping a source field breaks the build, while adding one is invisible. That is exactly the asymmetry you want at a boundary — additions are opt-in, removals are caught. If you prefer stricter derivation over full declaration, the middle path is a `StrictOmit<T, K extends keyof T>` alias that re-imposes the constraint `Omit` drops, so at least a typo or a rename fails. It restores the compiler error without restoring the opt-out default, which is why it is a partial answer rather than a complete one. ## The rule I would write down Derive inside a boundary; declare across one. Inside a module, coupling is the point and the compiler is your ally in propagating change. Across a boundary — service, team, published package — the type is a negotiated artefact, and the compiler's job changes from propagating your changes to *stopping* them. ## What an interviewer is listening for Not a rule but a decision procedure. Weak answers are absolutist in either direction: "always derive, DRY" or "never derive, decoupling". The strong answer names the axis — how the two types evolve, and in which direction you want a change to be loud — and shows awareness that `Omit`'s loose key constraint makes the failure silent rather than noisy. Mentioning that the response type does not strip anything at runtime, so the real serialization boundary needs its own explicit mapping regardless, shows you know the type layer is not the enforcement layer.
- If you declare the response type explicitly, how do you stop it drifting from the entity?Add a compile-time compatibility assertion rather than a derivation — for example a conditional type checking that the response extends `Pick<Entity, keyof Response>`, assigned to a dummy constant. Removing or retyping a source field then breaks the build, while adding one changes nothing. The direction of the check is what buys you opt-in exposure.
- Where do you still derive without hesitation?Inside a module boundary: component props that are a subset of state, form models over a domain object, PATCH payloads. There the two shapes are meant to move together, and derivation is what surfaces drift when the source gains a field. The coupling is the feature rather than the risk.
- Does an explicitly declared response type prevent the extra field being serialized?No. Types are erased, so neither approach removes anything at runtime — an object with extra properties still serializes them all. The type only records intent; the actual boundary needs a mapping function, a rest destructure, or a serializer with an allow-list. Treating the type as the enforcement mechanism is the mistake underneath both approaches.
saying these in an interview costs you the question
- Treating DRY as decisive without asking how the types evolve
- Assuming a derived response type prevents fields leaking at runtime
- Believing the compiler flags an Omit key that no longer exists
- Deriving public contracts straight from persistence entities by default
- Refusing all derivation, duplicating shapes that change together