The existence of /// reference directives in dependencies breaks explicit typeRoots ordering
🔎 Search Terms
typeroots triple slash directives ordering
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about reference directives
⏯ Playground Link
No response
💻 Code
We are explicitly including our own types in typeRoots before node_modules/@types:
{
"compilerOptions": {
"typeRoots": ["src/types", "node_modules/@types"]
because we're making some custom changes to js/node types. This has been working great until we recently tried to switch to vitest which includes vite which has:
/// <reference types="node" />
in a file, which breaks some of our types as we have conflicting interfaces with @types/node
🙁 Actual behavior
vitest ⇒ vite ⇒ node types is discovered before typeRoots is processed, meaning typeRoots ordering is effectively useless as any dependency can just force type injections before they're processed.
🙂 Expected behavior
typeRoots ordering should apply first, THEN types from dependencies
Additional information about the issue
No response
I'm not quite sure what you mean by typeRoots ordering being different -- do you mean the order that the types directives get processed? Can you post a sample repo we can look at?
I mean that the /// directive from src/file.ts ⇒ node_modules/vitest/somefile.ts ⇒ node_modules/vite/somefile.ts ⇒ node_modules/@types/node appears to be processed before typeRoots.
So the @types/node types are loaded before our types, despite our typeRoots setting telling them to load after.
The result is that our types throw errors about conflicts with @types/node since those are loaded first, and more importantly we can't actually replace certain types in @types/node (specifically any in interfaces that conflict). Simply commenting out import from vitest resolves it.
I'll make a sample repo shortly
typeRoots just affects how @types resolution proceeds; which dirs to look for @types in (the "type roots"). https://www.typescriptlang.org/tsconfig/#typeRoots
But this doesn't allow you to place your own files before others; things are still loaded in reverse dependency order.
I think the fix is either to make your types not conflict, patch @types/node in your environment, or convince vite to not load the node types.
We can't patch @types/node, that's my entire point.
If vite is included, anywhere in a project, then @types/node is included before our own types, so it is impossible to patch conflicting interfaces.
I think the fix is either to make your types not conflict
erm... many @types/node types are slightly wrong, or too loose. Devs should be allowed to patch them.
If the types are wrong, can you send PRs to DT to fix them, or file discussions there? Or bring them up in the DT channel on the discord?
In any case, typeRoots is not the option for this; it's only intended to help control which places we look for @types packages. There is nothing in TS that can force certain files to come first in the compilation, except for lib.d.ts.
sample repo: https://github.com/a-non-a-mouse/0194011212380128310923810
I'm not trying to have a discussion about the correctness of DT :)
The fact is, even if we were to not install @types/node, and completely clone that repo, and make our own modifications to it, vite would install it, and those types would be loaded globally instead of ours.
This isn't just an issue for node types, it's also an issue of global type injection of whatever anyone wants into any parent repo, but that's a somewhat different discussion.
Your repo is just set up wrong, not sure this is an accurate bug repro. You have src/types/node/node.d.ts, but this isn't a valid resolution source for the node types directive; it needs to be src/types/node/index.d.ts. Once you fix that problem, there's no error in that file (and instead you get errors from the missing global types like Buffer that your custom one here doesn't have, thus demonstrating that the replacement works)
this isn't a valid resolution source for the node types directive
TS doesn't care what the file is named in this case (it would for imports, but I'm not importing anything from node in this sample repo), it's just a global type. It resolves it just fine (when vite is not imported).
it needs to be src/types/node/index.d.ts
❯ git clone https://github.com/a-non-a-mouse/0194011212380128310923810 ❯ cd 0194011212380128310923810 ❯ mv src/types/node/node.d.ts src/types/node/index.d.ts
Exactly the same thing.
Once you fix that problem, there's no error in that file
There's still an error.
instead you get errors from the missing global types like Buffer
Nope, nothing changes. If you're seeing that you obviously change other things, or your repo isn't isloated. You're describing an impossibility as the code in this repo doesn't know about Buffer, there's nothing referencing Buffer, and the only way an error like that could possibly surfaces is if the @types/node were already loaded, which is contradictory to this entire issue.
Let's simplify this:
- A dependency copies
@types/node - The dependency imports their local types with
/// <reference - If you import anything that touches that file, the global types in any parent project will use the types defined in the child project
- For things like conflicting interfaces that can't be merged, there will be an error hoisted somewhere, but it will be invisible because it'll be somewhere in node_modules, and everything will work
The end result is that it is effectively impossible to update @types/node in any parent project. This applies to any other global types as well.
I'm arguing that child projects should not have the authority to hijack any global type in any parent project. That globals should not leak out of anything not defined in typescript itself or typeRoots (though there are probably a million edge cases that would break doing this).
Resolving types in typeRoots first would at least solve part of the issue in that non-mergable types would have the correct types defined by the project's tsconfig, and not whatever child project happens to be resolved first.
Or, very simply:
Adding a new dependency should never cause other types to break.
TS doesn't care what the file is named in this case
It does! Run --traceResolution and you'll see what's happening
Exactly the same thing.
I'm seeing different behavior after doing this
I'm arguing that child projects should not have the authority to hijack any global type in any parent project. That globals should not leak out of anything not defined in typescript itself
This is a completely different issue than the one you're describing above and has nothing to do with typeRoots
Adding a new dependency should never cause other types to break.
This isn't really a feasible invariant, since two dependencies absolutely could declare conflicting globals
This is a completely different issue than the one you're describing above and has nothing to do with typeRoots
no, it's not. It's the exact same issue, I just phrased it differently. You're laser focused on the exact scenario I described as our use case (we're changing node types) instead of the general case.
This isn't really a feasible invariant, since two dependencies absolutely could declare conflicting globals
Yes, but there is a difference between "eh, whatever, let there be random chaos" and "controlled damage". You have two types of conflicting globals -- Those that are hard conflicts, and those that can be merged.
I'm simply arguing that if typeRoots is resolved first, the first type of conflict evaporates into the void for many common use cases, and it makes infinitely more sense. The 2nd type of conflict stays exactly like it is now and you just get merged definitions.
tsconfig.json's config is more important than some random thing a package 20 layers deep in node_modules includes does.
The actual separate discussion is why is anything outside of typeRoots allowed to pollute global at all. Their globals should be constrained at the package boundaries. But that's actually off topic ;)
MS engineers, again, too stupid to recognize an obvious bug 🤷♂️