Can’t resolve `@types` when vendoring dependencies for browser ESM imports
Bug Report
🔎 Search Terms
- native modules alias relative paths
- Relative references must start with either "/", "./", or "../".
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about declaration files
⏯ Playground Link
N/A (behavior is documented)
💻 Code
Related frustrations have been posted many times before - so I've hesitated to post this:
https://github.com/microsoft/TypeScript/issues/16577 https://github.com/microsoft/TypeScript/issues/13422 https://github.com/microsoft/TypeScript/issues/28288 https://github.com/microsoft/TypeScript/issues/42151
My case, and workaround, are documented here: https://stackoverflow.com/questions/67725840/how-can-i-use-threejs-es6-native-modules-with-typescript/73575993#73575993
🙁 Actual behavior
- The Declaration File must be included with an alias, e.g.
import * as THREE from 'three' - es6 modules require extensions, which the alias does not allow. TypeScript does and will not support URL rewriting.
- As a result, I cannot use Declaration Files + TypeScript without importing additional software
🙂 Expected behavior
There could be multiple possible solutions here:
-
Just document it better, here: https://www.typescriptlang.org/docs/handbook/declaration-files/consumption.html If this was described as a known limitation, it would have saved me half a day.
-
Support aliases with extensions in one of two ways: a) Allow the alias itself to be of the final expected form
./js/lib/three.module.jsb) Allow some sort of a custom mapping, which I can use to tell tsc to look for an.d.tsaliasthreewhen it sees./js/lib/three.module.js
Thanks in advance!
Related frustrations have been posted many times before - so I've hesitated to post this:
#16577 #13422 #28288 #42151
The initial problem linked to doesn't really seem to have anything to do with these issues. Like the problem here is that this doesn't work right?
// Could not find a declaration file for module './node_modules/three/src/Three.js'. '/home/jamesernator/projects/playground/node_modules/three/src/Three.js' implicitly has an 'any' type.ts(7016)
import * as THREE from './node_modules/three/src/Three.js'
But that this does, but this isn't syntactically valid for browsers:
// @types/three work
import * as THREE from 'three'
In principle, TypeScript probably could support the former by noticing node_modules/lib in any path and mapping it to node_modules/@types/lib although I'm not sure there's a massive demand given most people just use some kind've bundler or such. (In addition it would require @types to be written in such a way that file structures match fully even if "exports" is used).
An alternative solution would be to use import maps, although they're currently Chrome only, but if that limitation doesn't bother you OR you don't mind waiting for other browsers to support them then you could just do:
<script type="importmap">
{
"three/": "./path/to/node_modules/three/"
}
</script>
In which case import * as THREE from "three"; would ALSO WORK in the browser.
(For libraries more complicated than three.js such an import map is a bit too simple, in which case you'd need an actual node_modules -> importmap generator).
Thanks for the quick reply!
Yes, the linked issues are maybe only related but not the same (not that I've exhaustively read all the replies therein).
In principle, TypeScript probably could support the former by noticing
node_modules/libin any path and mapping it tonode_modules/@types/lib
This would require that THREE is installed as a node module right? Seems common but for something this fundamental still a limitation. (I believe I ended up making my own copy of THREE to get the modules in es6 style, so it's not on npm 🤷♂️).
I will readily admit my use-case is not a common one :). It's just that when I'm working on a hobby project, the page of ~15 possible build tools to research is one I want to avoid for a while, ha!
TIL about import maps! With a slight syntax tweak, your example worked perfectly:
<script type="importmap">
{
"imports": {
"three": "./js/lib/three.module.js"
"src/": "./js/"
}
}
</script>
Seems like a reasonable work around - probably by the time I want to support other browsers I'll have to add a JS build pipeline anyway..
this isn't syntactically valid for browsers
Small nitpick: It is syntactically valid, it's just semantically invalid (unless the browser supports import maps, as you note). This is just a pet peeve of mine so take it with a grain of salt. 😅
An alternative solution would be to use import maps, although they're currently Chrome only
Trying to make myself a bit more useful, I wanted to chime in on this that you can use es-module-shims to get import maps working on non-Chromium browsers. I've been using this in Oozaru, it works quite well. (@pehrlich2 this might be useful for you as well)
This isn’t a solution, but you will be probably be interested in watching #50153 (part of #50152). I’m definitely open to brainstorming good ways to resolve DT types for vendored dependencies once that lands.
My current thoughts are
- This should Just Work™ for mapping
./node_modules/*to./node_modules/@types/*at least in--moduleResolution minimal, but possibly in all modes? - But I’m interested in solving it for relative imports in general. If you have all your browser-compatible dependencies in
./vendor/*, you should be able to add types for them in./types/*if you want.
After thinking about it for about 30 seconds, I’m wondering if it needs to be more complicated than
{
"compilerOptions": {
"relativePaths": {
"./node_modules/*": ["./node_modules/*", "./node_modules/@types/*"]
}
}
}
where unlike paths, the key side prefix doesn’t need to be an exact string match of the module specifier, but rather a prefix match after both the module specifier and the key are resolved to an absolute directory path. That is, it should match both "./node_modules/foo/index.js" and "../../../node_modules/foo/index.js" as long as both mentions of node_moduels refer to the same directory that the tsconfig mentioned.
Ok, so the problem with doing this with node_modules / npm packages specifically is that package.json files can contain complex intra-package mappings for types in themselves, so while the scheme I wrote above might work for relative paths in general, types inside packages cannot always be linked up just by relative paths. With this scheme, you’d immediately hit two different kinds of trouble:
- Packages that ship their own types but use
exportsortypesVersionsto put them in a different folder from the implementation.import "./node_modules/rxjs/dist/esm/index.js"doesn’t know to look up types from./node_modules/rxjs/dist/types/index.d.tswithout looking at the package.json. - Any
@typespackage that usestypesVersionsto ship multiple copies of the types for different TypeScript versions wouldn’t work without looking at that package.json.
There’s no way around stuff in node_modules being special; even if your module specifier into it doesn’t use any special Node resolution magic, we won’t be able to find types without using some of those things.
So the question is, for a mode like --moduleResolution minimal, if you have a way like relativePaths above to set up types for your relative dependencies, do we also need to include any special behavior that lets relative imports into node_modules resolve types? Unfortunately I just don’t know if anyone is doing this, or has any desire to.
I don't have an example on hand, but can't this be fixed by a .d.ts file in your local project that re-exports the types from DT (@types scope) in a declare module "./lib/threejs.min.js" {...}? (I'm guessing at the syntax, the problem with composing this from my phone...)