Search code examples
javascriptnode.jstypescriptmonkeypatching

Do global augmentations need to be imported as a side effect in the consumer?


I have a package called core.error. In this package I have two files

global.d.ts

export {};

declare global {
  export interface Error {
    foo(): void;
  }
}

index.ts

Error.prototype.foo = function (this: Error): void {
 
};

export const dooFoo = (err:Error) : void => {
  err.foo();
}

Now I have this package as a dependency in my second project. It seems the global augmentation code only gets executed up when I do this.

import "core.error"

const err = new Error();
err.foo();

But when I try to import the dooFoo function only i will get a runtme TypeError saying "TypeError: err.foo is not a function".

import { dooFoo } from "core.error"

const err = new Error();
err.foo();

Is this expected behaviour? If so do I need to import twice from my module like this?

import "core.error"
import { dooFoo } from "core.error"

const err = new Error();
dooFoo(err.foo());

Is there any other solution to properly load and run my module code that monkeypatches globals?

Update

Readings done before asking

Update 2

The declaration file for index.ts looks like this

/// <reference types="node" />
export declare const dooFoo : (error: Error ) => void;

tsconfig.json looks like this

{
  "compilerOptions": {
    "module": "ES2022",
    "target": "ES2022",
    "allowJs": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "strict": true,
    "outDir": "./build"
  },
  "include": ["source"],
  "exclude": ["node_modules", "tests", "build"]
}

Solution

  • TypeScript will elide imports that it (correctly or incorrectly) detects as being unused in the importing module. That means when it compiles to JavaScript, the import statement will be completely absent.

    In your example you import dooFoo but never use it. And so Typescript thinks the import is useless and removes it. But TypeScript doesn't keep track of whether or not an imported module has side effects. So the elided import leads to the runtime error you're seeing.


    The currently recommended approach is to use the --verbatimModuleSyntax compiler option as released with TypeScript 5.0 and implemented in microsoft/TypeScript#52203. This essentially turns off import elision (with the exception of imports that use the type modifier where the developer is explicitly asking for import elision). Before TypeScript 5.0 there were other compiler options you could use to get this effect but they are no longer relevant and have been deprecated.

    If you turn on --verbatimModuleSyntax then your dooFoo import is present in the compiled JavaScript and you get the desired side effect behavior.

    Stackblitz link to code


    If you can't use this approach then you'll need to work around it. One way is, as mentioned in the question, to explicitly write import "core.error" to perform a side-effect-only import, which TypeScript understands as having side effects.

    Another way would be to leave the import alone but actually use dooFoo in your code so that TypeScript doesn't think it can elide anything:

    import { dooFoo } from "core.error"
    dooFoo; // <-- mention it somewhere    
    const err = new Error();
    err.foo();
    

    Stackblitz link to code

    This last way is probably the most normal alternative approach, since presumably you're going to actually use an imported function somewhere in your code.

    But I'd recommend using --verbatimModuleSyntax since it makes TypeScript behave more consistently, and only get elision when you explicitly ask for it with an import type statement.