250

I want to extend Express Session typings to allow use my custom data in session storage. I have an object req.session.user which is an instance of my class User:

export class User {
    public login: string;
    public hashedPassword: string;

    constructor(login?: string, password?: string) {
        this.login = login || "" ;
        this.hashedPassword = password ? UserHelper.hashPassword(password) : "";
    }
}

So i created my own.d.ts file to merge definition with existing express session typings:

import { User } from "./models/user";

declare module Express {
    export interface Session {
        user: User;
    }
}

But it's not working at all - VS Code and tsc don't see it. So I created test definition with simple type:

declare module Express {
    export interface Session {
        test: string;
    }
}

And the test field is working ok, so the import cause problem.

I also tried to add /// <reference path='models/user.ts'/> instead import but the tsc didn't see the User class - how can I use my own class in *d.ts file?

EDIT: I set tsc to generate definition files on compile and now I have my user.d.ts:

export declare class User {
    login: string;
    hashedPassword: string;
    constructor();
    constructor(login: string, password: string);
}

And the own typing file for extending Express Sesion:

import { User } from "./models/user";
declare module Express {
    export interface Session {
        user: User;
        uuid: string;
    }
}

But still not working when import statement on top. Any ideas?

Michał Lytek
  • 10,577
  • 3
  • 16
  • 24

6 Answers6

678

After two years of TypeScript development, I've finally managed to solve this problem.

Basically, TypeScript has two kind of module types declaration: "local" (normal modules) and ambient (global). The second kind allows to write global modules declaration that are merged with existing modules declaration. What are the differences between this files?

d.ts files are treated as an ambient module declarations only if they don't have any imports. If you provide an import line, it's now treated as a normal module file, not the global one, so augmenting modules definitions doesn't work.

So that's why all the solutions we discussed here don't work. But fortunately, since TS 2.9 we are able to import types into global modules declaration using import() syntax:

declare namespace Express {
  interface Request {
    user: import("./user").User;
  }
}

So the line import("./user").User; does the magic and now everything works :)

gpresland
  • 1,690
  • 2
  • 20
  • 31
Michał Lytek
  • 10,577
  • 3
  • 16
  • 24
  • 10
    This is the right way to do it, at least with the recent versions of typescript – Jefferson Tavares Jul 21 '18 at 14:33
  • 1
    This approach is the ideal solution when declaring interfaces that extend global modules such as Node's `process` object. – Teffen Ellis Oct 19 '18 at 19:34
  • 4
    Thanks, this was the only clear answer to fixing my issues with Express Middleware extending! – Katsuke Apr 22 '19 at 01:14
  • Thx, helped a lot. Had no other way to do this in an existing project. Prefer this over extending the Request class. Dziękuję bardzo. – Christophe Geers Jun 11 '19 at 19:33
  • 4
    Thank you @Michał Lytek I'm wondering is there any official documentation reference for this approach? – Gena Aug 01 '19 at 15:42
  • 5
    @Gena https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#import-types – Michał Lytek Aug 08 '19 at 10:01
  • If I manually insert `declaration` in already transpiled index.d.ts than it is picked up as expected. However I can't make TS to actually generate it from file during transpilation. Where should I place it or how should I reference it in the config? Folder with that `d.ts` is already in the `include` array in `tsconfig.json` – jayarjo May 21 '20 at 09:28
  • 1
    I'm still at dark. What if I have a boolean or string instead of a custom class? I can't import anything in that case. How do I work with that? – Sriram R Nov 30 '20 at 05:09
  • 6
    Why do we need to use the import() syntax, why not a regular import within the declare block? – gaurav5430 Feb 18 '21 at 21:17
  • @gaurav5430 I guess "import ... from ..." is runtime while d.ts declaration is compile time. Like "type a = import('package')" is also compile time – Anh Nguyen Jul 09 '22 at 13:54
62

Thanks to the answer from Michał Lytek. Here is another method I used in my project.

We can import User and reuse it multiple times without write import("./user").User everywhere, and even implements it or re-export it.

declare namespace Express {
    type User = import('./user').User;

    export interface Request {
        user: User;
        target: User;
        friend: User;
    }

    export class SuperUser implements User {
        superPower: string;
    }

    export { User as ExpressUser }
}

Have fun :)

h00w
  • 675
  • 6
  • 4
27

For sake of completeness:

  • if you have an ambient module declaration (i.e, without any top level import/export) it is available globally without needing to explicitly import it anywhere, but if you have a module declaration, you will need to import it in the consumer file.
  • if you want to import an existing type (which is exported from some other file) in your ambient module declaration, you can't do it with a top level import (because then it wouldn't remain an ambient declaration).

So if you do this: (https://stackoverflow.com/a/39132319/2054671)

// index.d.ts
import { User } from "./models/user";
declare module 'express' {
  interface Session {
    user: User;
    uuid: string;
  }
}

this will augment the existing 'express' module with this new interface. https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation

but then to use this you would have to import this in your consumer file, it will not be available by default globally like ambient declaration as it is no more an ambient declaration

  • so, to import an existing type exported from another file, you have to import it inside the declare block (talking about this example, in other examples where you are not declaring a module, you can import inline at other places)

  • to do this, you cannot use a regular import like this

    declare module B {
      import A from '../A'
      const a: A;
    }
    

    because in current implementation, the rules for resolution of this imported module are confusing, and hence ts does not allow this. This is the reason for the error Import or export declaration in an ambient module declaration cannot reference module through relative module name. (I am not able to find the link to the relevant github issue, if someone finds it, please edit this answer and mention. https://github.com/microsoft/TypeScript/issues/1720)

    Please note, you can still do something like this:

    declare module B {
      import React from 'react';
      const a: A;
    }
    

    because this is an absolute path import and not a relative path import.

  • so the only way to correctly do this in an ambient module is using the dynamic import syntax (https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#import-types)

    declare namespace Express {
     interface Request {
       user: import("./user").User;
     }
    }
    

    as mentioned in the accepted answer (https://stackoverflow.com/a/51114250/2054671)

  • you can also do a global augmentation with something like this:

    import express = require('express');
    import { User } from "../models/user";
    
    declare global {
        namespace Express {
            interface Session {
                user: User;
                uuid: string;
            }
        }
    }
    

    but remember global augmentation is only possible in a module not an ambient declaration, so this would work only if you import it in the consumer file, as mentioned in @masa's answer (https://stackoverflow.com/a/55721549/2054671)


All the above points are valid for importing a module which is exported from somewhere else, in your ambient modules, but what about importing an ambient module in another ambient module? (This is helpful if you want to use an existing ambient declartion in your own ambient module declaration and make sure those ambient types are also visible in the consumer of your ambient module)

  • you can use the /// <reference types="../../a" /> directive

    // ambientA.d.ts
    interface A {
      t: string
    }
    
    // ambientB.d.ts
    /// <reference types="../ambientA.d.ts" />
    declare module B {
      const a: A;
      export { a };
    }
    

Links to other relevant answers:

gaurav5430
  • 12,934
  • 6
  • 54
  • 111
  • 3
    I noticed you use relative path in triple slash reference tag, what if I want to reference a .d.ts file inside node_modules folder? – Bruce Sun Feb 03 '22 at 04:11
5

UPDATE

Since typescript 2.9, you seem to be able to import types into global modules. See the accepted answer for more information.

ORIGINAL ANSWER

I think the problem you're facing is more about augmenting module declarations then class typing.

The exporting is fine, as you'll notice if you try to compile this:

// app.ts  
import { User } from '../models/user'
let theUser = new User('theLogin', 'thePassword')

It seems like you are trying to augment the module declaration of Express, and you are really close. This should do the trick:

// index.d.ts
import { User } from "./models/user";
declare module 'express' {
  interface Session {
    user: User;
    uuid: string;
  }
}

However, the correctness of this code depends of course on the original implementation of the express declaration file.

Pelle Jacobs
  • 2,379
  • 1
  • 21
  • 25
  • 1
    If I move the import statement inside I get error: `Import declarations in a namespace cannot reference a module.`. If I copy-paste your code I got: `Import or export declaration in an ambient module declaration cannot reference module through relative module name.`. And if I try to use non-relative path I can't locate my file, so I moved declarations folder to node_modules ad add path `"declarations/models/user"` but still the whole d.ts is not working - can't see own extension of express session in intelisense or tsc. – Michał Lytek Aug 28 '16 at 09:33
  • I'm not familiar with these errors, sorry. Maybe there is something different in your setup? Does this to compile for you? https://gist.github.com/pellejacobs/498c997ebb8679ea90826177cf8a9bad. – Pelle Jacobs Aug 28 '16 at 09:51
  • This way it works but still doesn't work in real app. I have there an express request object with session object and it have other type declared - in namespace Express not module 'express': https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/express-session/express-session.d.ts – Michał Lytek Aug 30 '16 at 09:02
  • 6
    It doesn't work for me either. Once I add the import statements to my tsd.d.ts file, the entire file stops working. (I get errors in the rest of my application for things defined in that file.) – Vern Jensen Dec 21 '16 at 22:02
  • 6
    I had the same problem. It works if you use the import in a declared module within your .d.ts: ```declare module 'myModule' {import { FancyClass } from 'fancyModule'; export class MyClass extends FancyClass {} }``` – Alexander Weber Jun 30 '17 at 18:13
  • This won't work. The accepted answer is the right way to get this done. – 16kb Mar 30 '19 at 14:34
1

Is it not possible just to follow the logic with express-session:

own.d.ts:

import express = require('express');
import { User } from "../models/user";

declare global {
    namespace Express {
        interface Session {
            user: User;
            uuid: string;
        }
    }
}

In the main index.ts:

import express from 'express';
import session from 'express-session';
import own from './types/own';

const app = express();
app.get('/', (req, res) => {
    let username = req!.session!.user.login;
});

At least this seems to compile without any issues. For the full code, see https://github.com/masa67/so39040108

masa
  • 2,762
  • 3
  • 21
  • 32
  • 1
    You must not import declaration files, because `tsc` won't compile them. They are meant to be in the compilation but not in the output – csakbalint Feb 12 '20 at 14:45
  • @csakbalint ain't that what `import type { Requestt } from 'express'` is for? – Frondor Jun 11 '21 at 09:04
1

Please have a look here:

https://stackoverflow.com/a/43688680/5412249

It is possible to declare types in a module (i.e. in a file that uses import/export) and have those types augmented (merged) into a global namespace.

The key is to put the type definitions inside a

declare global { ... }

Below is an example familiar to Cypress users:

// begin file: custom_command_login.ts

import { foo } from './utils';

Cypress.Commands.add('logIn', () => {
  
   // ...

}); 


// add custom command to Cypress namespace
// so that intellisense will correctly show the new command
// cy.logIn

declare global {
  namespace Cypress {
    interface Chainable {
       logIn();
    }
  }
}

// end file: custom_command_login.ts