skip to content

Your team wants request-scoped user data available as `req.user` throughout a TypeScript service by augmenting the web framework's `Request` interface globally. As the engineer setting the standard, how do you weigh that global augmentation against the alternatives?

level: principalimportance: should knowfreq 32%

answer

  1. the edit applies program-wide
  2. is the claim true everywhere?
  3. optional is the honest shape
  4. distinct type proves middleware ran
  5. blast radius across a monorepo

basics

~20 s

Global augmentation is convenient but unscoped and unverified: the property appears on every request in every file whether or not the middleware ran. Prefer it only for genuinely universal, optional data, and use distinct types or an explicit context for anything a handler must be able to rely on.

solid answer

~50 s

Augmentation edits a type for the whole program, so the real question is whether the claim is true everywhere. `req.user` is not: it exists only after one middleware runs, yet the augmentation applies to every request object in every file, including tests and unrelated libraries. My rule is to allow augmentation for data that is genuinely ambient and to type it **optional**, so the checker forces each call site to handle absence. Where a handler must rely on the value, express that in the type rather than in a convention — hand the authenticated handler a distinct `AuthenticatedRequest` type, or pass an explicit context object, so the compiler proves the middleware ran. I also centralise augmentations in one reviewed declarations file, keep the count small, and remember that none of it is checked at runtime.

go deeper

for a junior

Understand that augmenting a shared interface affects every file that uses that type, not just yours, and that optional typing reflects a value some code paths will not have.

for a middle

Explain the mechanics behind the tradeoff: merging is additive and program-wide, emits nothing, and cannot express "present only after this middleware", which is why optionality is the honest shape.

for a senior

Show how to make the compiler prove the middleware ran — a distinct authenticated request type produced only by the authenticating wrapper — and spot the non-null-assertion habit that quietly undoes an optional declaration.

for a principal

Own the policy: where augmentations live, how many the codebase tolerates, namespacing to avoid collisions, a hard rule for security-relevant values, and an audit for augmentations whose supplying code no longer exists.

## What you are actually deciding Module and global augmentation are program-wide edits to a type you do not own. Once `user` is on `Request`, it is on *every* `Request`: in handlers behind the auth middleware, in handlers in front of it, in health checks, in tests that build a bare request object, and inside any other library that types a parameter as `Request`. The mechanism has no notion of scope, and it has no runtime component — nothing forces anyone to assign the property. So the design question is not "can we augment" but "is the claim we are adding true across the entire program". That reframing settles most of these debates. ## The three options and what each costs **Global augmentation with an optional property.** `user?: User` is the honest version. Every call site is forced by the checker to handle `undefined`, which is exactly what the runtime allows. The cost is friction: handlers that genuinely run behind auth still have to narrow, and teams get tempted to reach for `!` at every use — which quietly restores the unsound version while looking safe. **Global augmentation with a required property.** Ergonomically lovely and a lie. It tells the checker something false about every request in the program, so a route registered before the middleware, or a test fixture without it, becomes a runtime `undefined` where the type promised a value. This is the option I would rule out by policy, because the failures it produces are silent and appear far from the declaration. **A distinct type at the boundary.** Keep the framework's `Request` untouched and give authenticated handlers their own type — `interface AuthenticatedRequest extends Request { user: User }` — produced by a middleware or wrapper whose signature is the only way to obtain it. Now the compiler proves that a handler taking an `AuthenticatedRequest` is only reachable through code that supplies the user. The cost is plumbing: a wrapper, a slightly heavier handler signature, and the need for everyone to use it. **An explicit context object.** Drop the augmentation entirely and thread a typed context (or use the framework's own store, if it has one) through the call chain. Most explicit, most testable, and the most work to retrofit into an existing codebase. ## How I would decide Ask four questions: 1. **Is the property universally present?** If yes, augmentation is defensible. If it depends on a middleware, route, or environment, the type must say so. 2. **Who else sees this type?** In a monorepo, an augmentation in one package can affect every package that shares the compilation, including ones whose maintainers never asked for it. The blast radius is a real cost. 3. **What happens when it is missing?** A missing analytics field is a nuisance; a missing `user` treated as present is an authorisation defect. The higher the stakes, the more the type should force the check. 4. **Can two augmentations collide?** Name collisions between your augmentation and a library's are hard to resolve because both declarations are outside your reach. Namespacing the added member — one `ctx` object rather than five loose fields — reduces the surface. ## The policy I would write - Augmentations live in one declarations file per package, reviewed like a public API, with a comment naming the middleware or host that supplies each member. - Added members are optional unless the value is genuinely present on every instance in the program. - Security-relevant values are never merely optional-on-the-shared-type: they get a distinct type that only the authenticating path can produce. - Prefer one namespaced member over several loose ones, to limit collisions. - Periodically audit: an augmentation whose supplying middleware was removed is invisible rot, since nothing fails when the promise stops being kept. ## The line to hold The seductive property of augmentation is that it makes a type convenient everywhere at zero apparent cost. The cost is that it also makes the type *less true* everywhere, and the type system's whole value is the accuracy of its claims. When accuracy and convenience conflict on a value that authorisation decisions depend on, accuracy wins — and the way to pay for it is a wrapper type, not a habit of asserting non-null at three hundred call sites.

  • Why is declaring the augmented property required rather than optional the option you would ban outright?
    Because it asserts something false about every request object in the program. A route registered before the middleware, a health check, or a test fixture all satisfy the type while carrying no user, so the failure is a runtime `undefined` in code the checker declared safe. For authorisation-adjacent data that turns a type-system convenience into a security defect.
  • If the team keeps the optional property, what pattern would you watch for in review?
    A spreading habit of `req.user!` at call sites. The non-null assertion silences the check the optional type was added to force, so the codebase ends up with the unsound behaviour of a required property plus the noise of the optional one. If handlers legitimately always have a user, that is evidence they should be taking a distinct authenticated type instead.
  • How does the calculus change in a monorepo where several packages share one compilation?
    The blast radius grows: an augmentation written for one service edits the type for every package in the program, including ones that never opted in and libraries typed against the framework. That argues for keeping augmentations in the leaf application rather than a shared library, namespacing added members under a single object, and reviewing them as a shared contract.

saying these in an interview costs you the question

  • Declares the added property required for convenience
  • Treats augmentation as scoped to the file that writes it
  • Assumes the augmentation forces middleware to run
  • Adds many loose fields instead of one namespaced object
  • Solves the optional type with non-null assertions everywhere

context