1

I'm trying to create a library which extends the express Response with a property warn() and when using the library I want to have the new property warn() available.

This is my tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "declaration": true,
    "target": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "baseUrl": ".",
    "paths": {
      "*": ["node_modules/*"]
    },
    "types": ["mocha"],
    "typeRoots": ["@types"]
  },
  "include": ["src/**/*", "test/**/*"]
}

This is my @types/express/index.d.ts:

import * as express from 'express'
import http from 'http'

export {}

declare global {
  namespace Express {
    export interface Response<ResBody = any>
      extends http.ServerResponse,
        express.Response {
      warn(): this
    }
  }
}

src/registerwarn.ts:

export function registerwarn() {
  return (_req: Request, res: Express.Response, next): void => {
    res.warn = (): Express.Response => {
      console.log("warning");
      return res;
    };
    return next();
  };
}

src/warnings.ts:

import { registerwarn } from './registerwarn'
export default [registerwarn()]

A test:

test/warningtest.ts:

import 'should'
import express, { Request, Response } from 'express'
import warnings from '../src/warnings'

describe('test', (): void => {
  const app = express()
  app.use(warnings)
  app.get('/', (req: Request, res: Response) => {
    res.status(200).warn().send({ some: 'content' })
  })
  it('should ', (done) => {
    done()
  })
})

Running tsc returns these errors:

src/registerwarn.ts:3:9 - error TS2339: Property 'warn' does not exist on type 'Response'.

     res.warn = (): Express.Response => {
         ~~~~

test/hello_tests.ts:9:21 - error TS2339: Property 'warn' does not exist on type 'Response<any>'.

     res.status(200).warn().send({ some: 'content' })
                     ~~~~


Found 2 errors.

error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

How can I get rid of these errors and make sure if somebody uses the library from npm doesn't get these warnings as well?

I created a repro here.

Lauren Yim
  • 12,700
  • 2
  • 32
  • 59
Alexander Zeitler
  • 11,919
  • 11
  • 81
  • 124

1 Answers1

1

There are two possible solutions here I have identified, although I am unable to comment whether this is the "best" solution available - these are merely what I came up with.

Solution I (recommended): Remove mocha from globally declared types in tsconfig.json

As per the documentation on tsconfig.json's compilerOptions.types, declaring mocha under types will significantly restrict the visibility of @types packages.

By default all visible ”@types” packages are included in your compilation. Packages in node_modules/@types of any enclosing folder are considered visible. For example, that means packages within ./node_modules/@types/, ../node_modules/@types/, ../../node_modules/@types/, and so on.
If types is specified, only packages listed will be included in the global scope. For instance:

{
  "compilerOptions": {
    "types": ["node", "jest", "express"]
  }
}

This tsconfig.json file will only include ./node_modules/@types/node, ./node_modules/@types/jest and ./node_modules/@types/express. Other packages under node_modules/@types/* will not be included.

By dropping the compilerOptions.types flag altogether, your typing should now be visible.

Within your test file, you should now add an import line for import 'mocha' to gain access to testing methods (e.g. describe, it, etc). This will need to be done for each test. I would recommend using an import, as it is a bit clearer where your declarations are coming from (and not invisibly popping up as a result of your tsconfig.json).

Additionally, I am not sure that you would want mocha to be declared globally here either, as mocha test imports would automatically be visible under files in the src folder. I suspect you might run into some other strange errors using this flag.

Note: I suppose you could alternative double-down with the types flag and add "express" to this list, which will locate your module augmentation under @types. Your mileage may vary significantly with this flag.

Solution II: Explicitly add your @types .d.ts declarations under the include field.

By specifying the compilerOptions.types flag, visibility is now removed from your custom @types directory. You can hack this back in by modifying the include flag from

"include": ["src/**/*", "test/**/*"]

to

"include": ["src/**/*", "test/**/*", "@types/**/*.d.ts"]

I am not too keen on this solution, as it does not address the underlying cause of @types not being visible. You may experience strange errors with this route.


After choosing a solution above (or possibly finding an alternative solution), you need to resolve a few typing errors in the project:

  • The typings for your warnings sources does not seem to be compatible with any app.use overload. tests/hello_tests.ts is complaining about no matching type overload for app.use(warnings).
  • You do not need to specify any extends for your Response interface - all interfaces under the same namespace will be merged. Since you are declaring an extends on express.Response, the same interface you are augmenting and merging, the interface declaration is now recursively referencing itself and causing an error: Type 'Response<ResBody>' recursively references itself as a base type.. Dropping this extends clause resolves this error and preserves the functionality you desire.
    declare global {
      namespace Express {
        export interface Response<ResBody = any> {
          warn(): this
        }
      }
    }
    

    Note that I preserved the generic in this instance, but it is optional. If any of your new functionalities require the generic, you should specify it (as demonstrated above).

After these typing issues are resolved, tsc exits successfully and reports no errors.


As an aside that is unrelated to your question, your prettier configuration is having a field day with CRLF/\r\n line endings (i.e. an error on every line). You should configure your configuration to be more relaxed, or specify a .gitattributes file in your Git repository so that line endings are LF/\n (and not CRLF/\r\n) by default on checkout.

concision
  • 6,029
  • 11
  • 29
  • Thanks for your extensive answer. I tried solution 1 and it seems to work in general. When using the library from another project, yet I have to manually add the `@types/express/index.d.ts`. – Alexander Zeitler Oct 03 '20 at 22:04
  • @AlexanderZeitler When compiling your TypeScript project, `.d.ts` files and "external" library types (e.g. `.d.ts` typings from `compilerOptions.typeRoots`) are *not* bundled into your library's outputted `.d.ts` module typings. You may need to move your Express.js module augmentation within your project sources (e.g. `src`) and use a `.ts` extensions instead (as `.d.ts` typings are *not* bundled) to be able to recognize these types in another project. You could alternatively just copy the `@types/express/index.d.ts` into your transpiled output directory after TypeScript compilation. – concision Oct 04 '20 at 02:32
  • @AlexanderZeitler Take a look into this StackOverflow post about the behavior of the TypeScript compiler with `.d.ts` files in your project: https://stackoverflow.com/a/56440335/14352161 – concision Oct 04 '20 at 02:33
  • 1
    Thanks again, you helped me a lot. – Alexander Zeitler Oct 04 '20 at 13:41