Recipe · Modules and packages

How do I add a property to a type from another package?

  • Node.js (ES modules)
  • Module augmentation
  • declare global
  • Express
  • .d.ts
  • Checked with TypeScript 5.9

The problem My authentication middleware sets req.user, and every handler reads it. tsc says Property 'user' does not exist on type Request. How do I add a property to a type that @types/express owns?

The short answer

Add a .d.ts file that extends the interface the package exposes for this. For Express it is the global Express.Request:

express.d.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
export {};

declare global {
  namespace Express {
    interface Request {
      /** Set by the authenticate middleware. */
      user?: { id: string; role: "customer" | "staff" };
    }
  }
}
Adding user to Express's Request type.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js", "*.d.ts"]
}

Every req in the project now has an optional user, set by the middleware and read by the handlers:

auth.js auth.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
import express from "express";

/** @satisfies {import("express").RequestHandler} */
const authenticate = (req, res, next) => {
  const token = req.get("authorization");
  if (token === "Bearer staff-token") req.user = { id: "u-1", role: "staff" };
  next();
};

const app = express();
app.use(authenticate);
app.get("/admin", (req, res) => {
  if (req.user?.role !== "staff") return res.status(403).end();
  res.json({ hello: req.user.id });
});
import express, { type RequestHandler } from "express";

const authenticate = ((req, res, next) => {
  const token = req.get("authorization");
  if (token === "Bearer staff-token") req.user = { id: "u-1", role: "staff" };
  next();
}) satisfies RequestHandler;

const app = express();
app.use(authenticate);
app.get("/admin", (req, res) => {
  if (req.user?.role !== "staff") return res.status(403).end();
  res.json({ hello: req.user.id });
});
Adding user to Express's Request type.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js", "*.d.ts"]
}

Why it works

Interfaces with the same name merge. @types/express declares Request so that it extends an (empty) global interface Express.Request; adding members to that global interface adds them to every Express request type. export {} makes the file a module, and declare global lets a module add to the global scope. The property is optional because routes that run before the middleware, or requests without a token, have no user.

The same thing in TypeScript

The TypeScript equivalent tab of the second example uses the same declaration file; only the handler moves from @satisfies to satisfies. Augmentations are declaration-file code in both kinds of project.

What goes wrong

Without the declaration, assigning req.user is TS2339: Express’s types do not know the property:

auth.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: auth.js(6,7): error TS2339: Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. auth.js(6,7): error TS2339: Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. auth.js(6,7): error TS2339: Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'. checked 21 October 2026
import express from "express";

/** @satisfies {import("express").RequestHandler} */
const authenticate = (req, res, next) => {
  req.user = { id: "u-1", role: "staff" };error TS2339: Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.
  next();
};

express().use(authenticate);
The same middleware without the declaration file.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

With it, the added property is checked like any other: a role outside "customer" | "staff" is TS2322:

auth.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: auth.js(6,27): error TS2322: Type '"admin"' is not assignable to type '"customer" | "staff"'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. auth.js(6,27): error TS2322: Type '"admin"' is not assignable to type '"customer" | "staff"'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. auth.js(6,27): error TS2322: Type '"admin"' is not assignable to type '"customer" | "staff"'. checked 21 October 2026
import express from "express";

/** @satisfies {import("express").RequestHandler} */
const authenticate = (req, res, next) => {
  req.user = { id: "u-1", role: "admin" };error TS2322: Type '"admin"' is not assignable to type '"customer" | "staff"'.
  next();
};

express().use(authenticate);
With the declaration, the added property is checked like any other.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js", "*.d.ts"]
}

Gotchas

  • The .d.ts must be in the program. List it in include; an augmentation in a file nobody includes has no effect.
  • Augmenting a package module needs export {} or an import. In a file without top-level import or export, declare module "express" declares a new module that hides the real one instead of extending it: express() is then TS2349, “This expression is not callable”.
  • res.locals is the typed alternative. Data for one request can also go in res.locals, typed with RequestHandler’s fifth type parameter; see the middleware recipe.

Questions

Why declare global { namespace Express … } and not declare module "express"?

@types/express builds its Request from a global Express.Request interface that exists for exactly this purpose. Other packages expose their types differently; their documentation or their .d.ts files say which interface to extend.

Can I augment a type from a JavaScript file?

No. Augmentation uses declare and interface merging, which only declaration files (or TypeScript files) can write. The .d.ts file sits beside your JavaScript and is listed in include.

Sources

Last reviewed 21 October 2026.