Include a class in the declaration file (*d.ts)

I am trying to enhance the Express Session typings to incorporate my custom data in session storage. In my code, I have an object req.session.user which is an instance of a class called User:

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

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

To achieve this, I created my own definition file named own.d.ts to merge definitions with existing express session typings:

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

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

However, it seems that it's not functioning properly - both VS Code and tsc are unable to detect it. To test, I added a simple type field as follows:

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

Surprisingly, the test field works fine while importing the User class causes issues.

I also attempted to use

/// <reference path='models/user.ts'/>
instead of import, but the tsc did not recognize the User class. How can I successfully utilize my own class in a *d.ts file?

EDIT: After configuring tsc to generate definition files during compile, I now have my user.d.ts:

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

Additionally, here is the typing file for extending Express Session:

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

Despite these changes, the issue persists when using the import statement at the top. Any suggestions?

Answer №1

It took me two years of working with TypeScript to finally crack this problem.

In TypeScript, there are two types of module declarations: "local" (normal modules) and ambient (global). The latter allows for the declaration of global modules that can be merged with existing module declarations. What sets these files apart?

d.ts files are considered ambient module declarations only if they do not contain any imports. Once an import line is provided, the file is treated as a normal module instead of a global one, making it impossible to augment module definitions.

This is why none of the previously discussed solutions were successful. However, starting from TS 2.9, we have the ability to import types into global module declarations using the import() syntax:

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

With the line import("./user").User;, the magic happens and everything falls into place :)

Answer №2

A big shoutout to Michał Lytek for the helpful solution. I recently discovered another technique that has proved useful in my own project.

By importing User, we can easily utilize it multiple times without having to repeatedly type import("./user").User throughout our codebase. This approach also allows us to seamlessly extend or re-export the imported module.

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 }
}

Enjoy coding! :)

Answer №3

In the interest of covering all bases:

  • If you have an ambient module declaration (i.e., without any top-level import/export), it is globally available without requiring explicit import, but with a module declaration, you must import it in the consumer file.
  • If you wish to import an existing type (exported from another file) into your ambient module declaration, you cannot do so with a top-level import as it would cease being an ambient declaration.

For example: ()

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

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

However, to utilize this, you need to import it into your consumer file since it no longer qualifies as an ambient declaration and won't be globally available by default.

  • Therefore, to import an existing type exported from another file, you must do so inside the declare block (in this example). In other scenarios where you aren't declaring a module, you can import inline elsewhere.

  • To accomplish this, you cannot use a standard import like this:

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

    Due to current implementation rules causing confusion in resolving imported modules, TypeScript doesn't permit this. This results in the error message

    Import or export declaration in an ambient module declaration cannot reference module through relative module name.
    (If someone locates the relevant GitHub issue link, kindly edit this answer and include it. https://github.com/microsoft/TypeScript/issues/1720)

    Note that you can still do something like this:

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

    As this is an absolute path import rather than a relative one.

  • The only appropriate approach within an ambient module is using 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 recommended in the accepted answer ()

  • You can also perform global augmentation like this:

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

    Remember, global augmentation is only feasible in a module, not an ambient declaration. It will work only if imported into the consumer file, as mentioned in @masa's answer ()


All the points discussed above pertain to importing a module exported from elsewhere in your ambient modules. But what about importing an ambient module into another ambient module? (Useful for utilizing an existing ambient declaration in your own ambient module declaration and ensuring those ambient types are visible to consumers of your ambient module)

  • You can utilize 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 informative answers:

  • Ambient declaration with an imported type in TypeScript

Answer №4

UPDATE

As of typescript 2.9, it appears that you can now import types into global modules. Refer to the approved response for more details.

REVISED ANSWER

The issue at hand seems to be more related to extending module declarations rather than class typing.

The exporting process is working correctly, as evidenced by successful compilation with the following code:

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

It looks like you are attempting to extend the module declaration of Express, and you are very close. This modification should solve the problem:

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

However, the accuracy of this code relies on the original implementation of the express declaration file.

Answer №5

Is it possible to simply follow the logic using 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;
});

This code appears to compile without any issues. For the complete code, visit https://github.com/masa67/so39040108

Answer №6

Take a look at the following link:

You can define types within a module (such as in a file that uses import/export) and then merge those types into a global namespace.

The trick is to place the type definitions inside a

declare global { ... }

Here's an example that will be familiar to users of Cypress:

// start 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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The assignment of Type Observable<Observable<any[]>> to Observable<any[]> is not valid

Working on implementing autocomplete using data from a database service: @Injectable() export class SchoolService { constructor(private db: AngularFirestore) { } getSchools(): Observable<School[]> { return this.db.collection<School> ...

Sharing the input value with a service in Angular 4

I am a beginner when it comes to Angular 4. I currently have a variable named "md_id" which is connected to the view in the following way. HTML: <tr *ngFor="let item of driverData"> <td class="align-ri ...

What is the maximum allowable size for scripts with the type text/json?

I have been attempting to load a JSON string within a script tag with the type text/json, which will be extracted in JavaScript using the script tag Id and converted into a JavaScript Object. In certain scenarios, when dealing with very large data sizes, ...

Can an entire object be bound to a model in an Angular controller function?

In TypeScript, I have defined the following Interface: interface Person { Id: number; FirstName: string; LastName: string; Age: number; } Within a .html partial file, there is an Angular directive ng-submit="submit()" on a form element. A ...

Calculating the percentage difference between two dates to accurately represent timeline chart bar data

I am in the process of creating a unique horizontal timeline chart that visually represents the time span of milestones based on their start and finish dates. Each bar on the timeline corresponds to a milestone, and each rectangle behind the bars signifies ...

Showing json information in input area using Angular 10

I'm facing an issue with editing a form after pulling data from an API. The values I retrieve are all null, leading to undefined errors. What could be the problem here? Here's what I've tried so far: async ngOnInit(): Promise<void> ...

ReactJs Error: Unable to access property value because it is undefined (trying to read '0')

I am currently attempting to retrieve and display the key-value pairs in payload from my JSON data. If the key exists in the array countTargetOptions, I want to show it in a component. However, I am encountering an error message stating Uncaught TypeError ...

Is it feasible to develop a TypeScript module in npm that serves as a dual-purpose tool, functioning as both a command line utility

My goal is to develop an npm TypeScript module that serves dual purposes - acting as a command line utility and exporting methods for use in other modules. The issue arises when the module intended for use as a command line utility requires a node shebang ...

ESLint appears to be thrown off by TypeScript type assertion within .vue files

Instructing TypeScript to recognize my ref as a canvas element is crucial for it to allow me to call getContext. To achieve this, I utilize the following code: this.context = (<HTMLCanvasElement>this.$refs.canvas).getContext('2d'); Althou ...

How can I provide type annotations for search parameters in Next.js 13?

Within my Next.js 13 project, I've implemented a login form structure as outlined below: "use client"; import * as React from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { signIn } from "n ...

The HttpPut request code format is malfunctioning, although it is effective for another request

I'm struggling with a HTTPPUT request that just won't get called. Strangely enough, I have a similar put request that works perfectly fine for another tab even though both pages are practically identical. I've exhausted all options and can&a ...

Next.js encountered an error when trying to locate the 'net' module while working with PostgreSQL

I'm facing a challenge in my Next.js project while attempting to retrieve all records from a table. The error message I'm encountering is "Module not found: Can't resolve 'net'" with an import trace pointing to multiple files withi ...

Is it possible for an uninitialized field of a non-null literal string type to remain undefined even with strict null checks in

It seems that there might be a bug in Typescript regarding the behavior described below. I have submitted an issue on GitHub to address this problem, and you can find it at this link. The code example provided in that issue explains the situation more clea ...

The error message "Unable to access property MyToast.java as it is not

I've been diligently following this tutorial on how to write Java code in NativeScript and use it directly in TypeScript. But, unfortunately, I encountered an error message stating: Cannot read property 'MyToast' of undefined app.component ...

Utilizing React Typescript for Passing Props and Implementing them in Child Components

I'm currently working with React and TypeScript and attempting to pass data as props to a child component for use. However, I've encountered an error that I can't quite understand why it's happening or how to resolve it. Additionally, I ...

Could adjusting the 'lib' compiler option to incorporate 'dom' potentially resolve TS error code TS2584?

My preferred development environment is Visual Studio where I write Typescript code. I am facing an issue where I simply want to use the command "document.write" without encountering an error. Unlike my previous PC and VS setup, I am now getting an error ...

Encountering a sort error in Angular 8 when attempting to sort an Observable

I am struggling to organize an observable that is retrieved from a service using http.get. I have attempted to sort the data both from the service and in the component after subscribing. However, I keep encountering the error "Sort is not a function". As ...

Dissimilarities in behavior between Angular 2 AOT errors

While working on my angular 2 project with angular-cli, I am facing an issue. Locally, when I build it for production using ng build --prod --aot, there are no problems. However, when the project is built on the server, I encounter the following errors: . ...

Button for enabling and disabling functionality, Delete list in Angular 2

I am looking to toggle between the active and inactive classes on a button element. For example, in this demo, there are 5 buttons and when I click on the first button it removes the last one. How can I remove the clicked button? And how do I implement the ...

Issues arise with Jest tests following the implementation of the 'csv-parse/sync' library

Currently utilizing NestJs with Nx.dev for a monorepo setup. However, I've come across an issue after installing csv-parse/sync - my Jest tests are now failing to work. Jest encountered an unexpected token Jest failed to parse a file due to non-stand ...