skip to content

Your team proposes augmenting a third-party library's exported interface in TypeScript so a field it does not declare becomes visible everywhere. What do you weigh before accepting that over defining a local type?

level: principalimportance: nice to knowfreq 26%

answer

  1. it is not a local change
  2. who else sees this type
  3. two packages, one member name
  4. the type promises what nothing enforces
  5. optional is the honest declaration

basics

~20 s

Augmentation is program-wide, unconditional and unenforced: every file sees the field whether or not it imports yours, two packages adding the same name collide hard, and the type promises something no runtime code checks. Prefer a local type unless third-party code must see the change.

solid answer

~50 s

I weigh four things. **Blast radius** — an augmentation applies to the whole compilation; you cannot scope it to a folder or a team, and a reader finds a property with no declaration in the package. **Collision risk** — if another dependency, or a later version of the library itself, adds the same member with a different type, that is a hard merge error nobody clearly owns. **Honesty** — types are erased, so declaring `user: User` asserts a guarantee no code enforces; if the middleware did not run you read `undefined` from something typed non-optional, which is why such fields belong declared optional. **Necessity** — augmentation is only required when code you do not control must see the enriched type. If it is only your own call sites, a local intersection, a wrapper type, or a typed accessor that returns the narrowed shape keeps the change visible and reversible.

go deeper

for a junior

Know that augmenting a third-party interface changes the type for the whole program, not just your file, and that it never creates the value at runtime.

for a middle

Explain the concrete failure modes — a colliding member from another package, a library version that adds the same name, a field declared required but never set — and why optional is usually the honest declaration.

for a senior

Choose deliberately between augmentation, a local intersection, a wrapper, and a validating accessor, and justify the choice by who must see the type and what actually establishes the guarantee.

for a principal

Own it as policy: augmentation is unscoped, hard to reverse, and travels to consumers of anything you publish, so define who may augment which surfaces, require owner-specific naming, and review published augmentations as public API.

## What you are actually proposing A module augmentation reopens another package's declaration space and merges members into one of its interfaces. It is the same mechanism as any interface merge — powerful precisely because it is not scoped. Understanding it as "editing the library's types for the entire program" rather than "adding a field in my file" is the whole of the judgment call. ## Blast radius Once the augmenting file is part of the compilation, the added member exists for every file in it. There is no import required, no way to restrict it to one directory, and no per-consumer opt-out. Consequences worth naming: - **Discoverability inverts.** A reader hovering the property sees a member that does not appear in the package's own declarations, and there is no local clue about who added it or under what conditions it is populated. - **It travels.** If you publish a library whose own declarations carry a global augmentation, every consumer of your package inherits it whether they wanted it or not. Imposing a change on downstream programs is a decision, not a detail. - **It cannot be staged.** You cannot roll it out to one team first; the compilation is all-or-nothing. ## Collision risk Merging has no precedence rule for properties: two declarations of the same member name with different types is an error, full stop. Three ways that bites in real programs: 1. Two dependencies both augment a popular interface and both pick the obvious name. Neither is wrong, neither can be un-applied, and the build stops. 2. Your augmentation and a colleague's, in the same monorepo, differ on required-versus-optional or on the exact shape. 3. The library itself later ships that member with its own type, and a routine version bump breaks compilation with an error that points at your file rather than at the change that caused it. The cheap mitigations are conventions, not mechanisms: give the added member an owner-specific name rather than a generic noun, or add a single namespaced member holding everything your feature needs, so the surface you occupy is one name you control. ## The honesty problem Everything in the type layer is erased. An augmentation emits nothing and verifies nothing; it changes only what the checker believes. Declaring a required field asserts a guarantee that some code, somewhere, always ran before this value was observed — and nothing enforces it. Handlers reached by a path that skips the setup read `undefined` through a type that says it cannot be. So if the field's presence is conditional, declare it optional. That pushes the uncertainty into the type where callers must handle it, at the cost of a check at each use — which is the correct cost, because the uncertainty is real. If you want the checked-once, non-optional experience, express it as a *distinct type* the code passes through a validating boundary, so something at runtime actually establishes the guarantee before the type asserts it. ## The alternatives, and what each costs - **Local intersection at the call site** — `LibType & { tenantId: string }`. Visible, scoped, and requires a cast or a narrowing step at the boundary. Best when a handful of sites need it. - **A wrapper type of your own** that holds the library value plus your data. Fully honest and fully local, at the cost of unwrapping. - **A typed accessor** — a function that takes the library value and returns your enriched type, throwing or narrowing if the data is absent. This is the option that makes the runtime check and the type claim agree, and it is usually the right answer when the field comes from middleware or setup that may not have run. - **The augmentation** — the only option when third-party or framework code, which you cannot edit, must itself see the enriched type. Plugin ecosystems designed around "packages contribute fields" are the legitimate case: the library's own design anticipates augmentation and documents the member names. ## How I would decide Ask who needs to see the type. If the answer is "only our code", use a local type — it keeps provenance obvious and can be deleted in one place. If the answer is "the framework, at call sites we do not own", augmentation is the right tool, and I would then insist on: an owner-specific or namespaced member name; optional unless a runtime boundary genuinely guarantees presence; the augmentation living beside the code that populates the field, not in a shared types dump; and a note in the module explaining what sets it. If we publish libraries, I would additionally treat any augmentation reaching consumers as a public API decision reviewed like any other breaking-change surface — because for the consumer, that is exactly what it is.

  • Two dependencies augment the same interface with the same member name and different types. What are your options?
    Few and unpleasant, which is why prevention matters. You cannot un-apply an augmentation or scope it away. Realistically: pin or patch one dependency's types, fork or wrap one of the packages, or push a fix upstream to rename or namespace the member. That asymmetry — cheap to add, expensive to undo — is the main argument for owner-specific member names from the start.
  • Why do you insist the added member be optional when it is set by middleware that always runs today?
    Because the type layer is erased and enforces nothing. "Always runs today" is a property of the current wiring, not of the type, and one new route or test harness that skips the setup turns a non-optional declaration into a lie the compiler will defend. Optional makes the uncertainty explicit; if you want non-optional, establish it at a validating boundary that returns a distinct type.
  • Is it acceptable for a library you publish to carry a global augmentation in its own declarations?
    Only as a deliberate public-API decision. Your consumers inherit it with no opt-out and may hit collisions you never see, so it should be documented, namespaced under a name you own, and treated as a breaking-change surface on every release. When the enrichment is only for the library's internals, keep it in a local type instead.

saying these in an interview costs you the question

  • Treats augmentation as a local, file-scoped change
  • Assumes the added field is guaranteed to exist at runtime
  • Picks a generic member name on a widely shared interface
  • Ignores that published augmentations reach every consumer
  • Says a colliding augmentation can simply be overridden

context