@types/react
in a Mono Repo
The past few weeks my team and I have been dealing with the absolute nightmare that is developing a React 18 application in a React 17 mono repo.
We’ve dealt with issues with the @types/react
package in the past. These issues were easily solved when our whole mono repo is on the same version of React (17). Now that we are moving forward to React 18 things are starting to get hairy.
You can see a reproduction of the issue in this repo. A simple reproduction is created by creating two Vite/React applications in a pnpm
monorepo with one on React 17 and one on React 18 and then pulling in @headlessui/react
(or really any library that doesn’t include its own version of @types/react
).
As soon as you pull in @headlessui/react
in the React 18 instance you are met with a nefarious error:
src/App.tsx:20:8 - error TS2322: Type '{ children: Element[]; }' is not assignable to type 'CleanProps<ExoticComponent<{ children?: ReactNode; }>, never>'.
Property 'children' is incompatible with index signature.
Type 'Element[]' is not assignable to type 'never'.
20 <Disclosure>
~~~~~~~~~~
Found 1 error in src/App.tsx:20
My next step was to go to Google, searching for this error yields many results:
Namely - as this Stack overflow points out - the type interface for Function Components has changed to require you to provide your own children
prop.
So let’s check our package.json
to see what version of @types/react
and react
we are including.
"dependencies": {
"react": "18.2.0"
},
"devDependencies": {
...
"@types/react": "^18.2.15"
}
This looks right! But I have build errors!? How can this be? This is the problem we’ve been digging into for days and I’m hoping to shed some light on.
How did we get here?
The first question is to ask how did we get here? I have dependencies in my package.json
yet TypeScript is not respecting those and its resolving to a whole different version of @types/react
in another package.json
inside my mono repo.
The Breaking Change
The first issue in our chain of technologies that cause this behavior is that @types/react
(and @types/react-dom
) shipped a breaking change in React 17 -> 18. Seems benign right? A breaking change semver version with a breaking change, seem safe. For the most part, this is true, however shortly after the new version was shipped GitHub user gurkel83 raised an issue on the React repo https://github.com/facebook/react/issues/24304.
The explanation for this change can be found here. At time of writing the origin React repo issue has 47 comments, and numerous upvotes/reactions. Dan Abramov rightly points out that this is not an issue with React but with the DefinitelyTyped repo. React is written in Flow and as such does not ship its own TypeScript typings. If it did - this problem would likely have never happened.
Shouldn’t a breaking change be safe?
At first glance it seems like this change was safe - and while its fair to say it would be a huge upgrade for some libraries - it is not different than any breaking change in any other npm
package.
However, this issue in particular was exacerbated by the fact that, as GitHub user KutnerUri points out:
I have a React 15 project that’s happy with not upgrading to React 18 right away. Because a LOT of packages depend on
"@types/react": "*"
, version 18 is getting installed without anyone using it!
This meant that packages depending on @types/react: "*"
broke instantly on a fresh install after the new version was published. No matter what version of React you were on, or even what version your dependencies were on, TypeScript was now throwing errors like the above.
Notably, some packages that manually specified their required versions of @types/react
like @testing-library/react
were exempt from this issue, you can read up on that on this pull request.
Using Resolutions
There’s a few simple solutions, that work great in a non-monorepo scenario. The first one being yarn
/pnpm
overrides
(in yarn
called resolutions
) if you set the following in your root-level package.json
in a pnpm
workspace:
"pnpm": {
"overrides": {
"@types/react": "17.0.3"
}
}
This will effectively fix the issue by always resolving @types/react
to 17
or any version of your choosing. The problem with this solution is that nothing in your mono repo can use itself, or consume any package that actually needs React 18.
What if I want to co-locate React 17 and 18?
This is where things get trickier, there’s two options for allowing React 17 and 18 to coexist in a mono repo.
.pnpmfile.cjs
This approach requires you to add an additional dependency to any package that needs to resolve to types React 18.
The .pnpmfile.cjs
looks something like this:
function readPackage(pkg, context) {
if (pkg.name === "recharts" || pkg.name === "@types/recharts") {
pkg.peerDependencies["@types/react"] = "18.2.17";
pkg.peerDependencies["@types/react-dom"] = "18.2.7";
return pkg;
}
return pkg;
}
module.exports = {
hooks: {
readPackage,
},
};
In our case - we needed to use recharts
in a pnpm
project that was on React 18.
This approach can be helpful in a pinch but ultimately becomes untenable as you will have to keep this list in sync as you add new dependencies.
Package Extensions
Another solution is to use pnpm
"packageExtensions"
to explicitly define peerDependencies
or dependencies
for the projects you want to use @types/react@18
(or 17 if you are targeting the other way).
"pnpm": {
"packageExtensions": {
"@headlessui/react": {
"peerDependencies": {
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7"
}
}
}
},
In this example, pnpm
will assure that there is the specified version of @types/react
installed “next to”. @headlessui/react
. This has similar problems as the .pnpmfile.cjs
option as it is cumbersome to keep this extension list up to date. Therefore, the easiest option is to use tsconfig.json
to specify the exact path to resolve the typings of the react
library.
tsconfig.json
“paths”
This is the easiest solution as it fixes the issue across the board for your TypeScript project. This will tell TypeScript to resolve typings for "react"
to a specific folder, in this case the location of @types/react
included in your package.json
.
// tsconfig.json
"compilerOptions": {
...
"paths": {
"react": ["./node_modules/@types/react"]
},
}
This workaround, while unfortunate, seems to be the best option going forward. Tell TypeScript that it should always resolve to the version in your local package.json
.
What’s Next?
It doesn’t look like the React Team will rewrite or convert the code to TypeScript any time soon, meaning we are stuck with one of the above solutions for the immediate future. For the meantime though it makes sense for library authors and the DefinitelyTyped
repo to stop using "@types/react": "*"
. There is a GitHub Issue on the DefinitelyTyped
repo to provide options other than "*"
when a package uses transitive types in DefinitelyTyped
.
Hopefully we never have to deal with a transition like this again, but if we do, we at least have some options to remediate the issue.
I’ve created a test repo that you can play around with trying different options for fixing this issue, you can find it here.
If you have any questions on this or want to discuss reach out!