How to fix npm packages

Web development is full of external dependencies and frontend developers and NodeJs developers are especially depend on node_modules which we know are the heaviest objects in the universe.

node_modules are heaviest objects in the universe

Recently I faced with a critical issue with one of my code packages - NextJs caused a compiler error, so I was unable to build my app with TypeScript.

Since NextJs is a core dependency of my project, I cannot replace it with something else, so I have to find a workaround on fixing npm package in a quick way. Issue details will be explained below, but first let's define general goals on the fix:

  • Solution has to be explicit, universal and elegant
  • Solution has to be git tracked
  • Without Pull Request on GitHub repo (no time to wait for maintainers attention and approval)
  • Without forking GitHub repo to create my own version of package (I will not maintain this package as good as maintainers of official NextJs framework)
  • Solution has to be easy maintained after package version upgrade

Patch-package to the rescue!

Hopefully, some good developers from a community already took care about my case - patch-package is an npm package which patches source code of any package as if I would directly commit changes to package while still git ignoring node_modules.

Solution demo

Solution was very simple and fully aligned with general goals defined abode.

Screenshot below shows a full needed changes to my package to build the app without compiler errors. (if you ignore description comments, solution is just a few lines of code).

fix NextJs global.d.ts via patch-package

Issue use case

In my situation, the issue was with global typings .d.ts files placed deeply inside git ignored source code of npm dependency - typings of CSS modules are defined in an incompatible way with my tsconfig.json restrictions.

Original NextJs global.d.ts file makes all classes imports from *.module.scss in form of string record:

const classes: { readonly [key: string]: string }

What is wrong with it? It is weakly typed - you can refer to any arbitrary class name in your code and by default TypeScript compiler will be fine with that allowing potential runtime bugs, especially in future:

import style from './index.module.scss';

// no error by compiler πŸ‘Ž
<div className={style.nameWithTypo} />
// no error by compiler πŸ‘Ž
<div className={style.anotherTypoMissedHere} />
// no error by compiler πŸ‘Ž
<div className={style.previouslyDefinedButRemovedOrRenamedNow} />

Hopefully, we can make TypeScript compiler more stricter and force us to check all arbitrary members of objects via enabled option noUncheckedIndexedAccess in tsconfig.json. With this option, all our classes from module css files are potentially undefined.

So now, every object member checked by compiler and raise compile errors in cases like that:

import clsx from 'clsx';
import style from './style.module.scss';

    <Button
      className={clsx({
        [style.autoMargin]: props.fullWidth,
//         ❌  ^ A computed property name must be of type 'string', 'number', 'symbol', or 'any'.ts(2464)
      })}
      />
      {children}
    </Button>


// error is equivalent to
const demo = { [undefined]: true };
//           ❌ ^ A computed property name must be of type 'string', 'number', 'symbol', or 'any'.ts(2464)
TypeScript compiler error when undefined value used as object key

In code above, style.autoMargin potentially undefined and therefore not allowed to be used as key for literal object.

Of course I do not want to check in runtime existence of my class name. Also I do not want to lie to my compiler via terrible non-null assertion operator:

const panic = <T extends unknown>(value: T): NonNull<T> => {
  if (value === undefined || value === null) {
    throw new Error('value undefined');
  }
  return value;
}

// πŸ‘Ž runtime check via panic looks like over engineering
<div className={panic(style.autoMargin)} />
// πŸ‘Ž non-null assertion is a lie to compiler, do not spoil the code
<div className={style.autoMargin!} />
Ugly workarounds on potentially undefined values

So, the solution is to change module css typings

declare module '*.module.scss' {
  const classes: any
  export default classes
}

Now we don't need to handle potentially undefined values πŸŽ‰

But wait, your classes: any is even worse than a string record

Not exactly, because I use other technique to keep my CSS modules names relevant - TypeScript plugin typescript-plugin-css-modules.

πŸ’‘
typescript-plugin-css-modules shows a fully typed object of CSS classes available

This way, my favourite IDE VS Code helps me to know relevant CSS classes and highlight issues like typos.

See CSS module classes typed

I believe this approach is a good balance between development convenience and type reliability. For something more important than classnames, I would rely on real compiler check (no lies via :any or ! or as SomeType) or even runtime check like with a panic util above.