TypeScript doesn't seem to be resolving node_modules/@types if using compilerOptions.paths and the node_modules folder is not an ancestor of the file the imports a dependency.
I have the following structure:
node_modules
@types/lodash
lodash
main.ts
Inside main.ts I import the file like that:
import * as _ from '@scoped/core-libs/lodash';
and I configured paths in tsconfig.json:
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"baseUrl": ".",
"paths": {
"@scoped/core-libs/*": [
"testing/node_modules/*"
]
}
}
Although the module lodash itself is resolved I get an error:
error TS7016: Could not find a declaration file for module '@scoped/core-libs/lodash'.
I debugged sources a bit and it seems that the loadModuleFromNodeModulesOneLevel function gets the directory of the importing file and the @types is searched upwards :
function loadModuleFromNodeModulesOneLevel(extensions: Extensions, moduleName: string, **directory**: string, ...)
the node_modules/@types from the compilerOptions.paths is never checked.
This the intended behaviour, if you specify paths the compiler will follow them to reolve your module, and it will not try to look them up in @type.
you can add a fallback case in your path mapping for @types. i.e.:
"compilerOptions": {
"paths": {
"@scoped/core-libs/*": [
"testing/node_modules/@types/*"
"testing/node_modules/*"
]
}
}
@mhegazy , thanks a lot for the prompt response!
So as I understand the TS compiler treats d.ts modules as first class citizens just like .ts files instead of simply being a complement to .js files?
Also, as I understand from debugging, when using paths the package.json is still checked for typings field and if found it's used as a module resolution?
I tried that approach with another library jsep with the typings field:
node_modules
...
jsep
typings
tsd.d.ts
main.ts
but when importing the library the same way as lodash:
import * as jsep from '@scoped/core-libs/jsep';
I get an error:
Error:(2, 20) TS2306: File '.../node_modules/jsep/typings/tsd.d.ts' is not a module.
Here is the reference to the typings file.
Any help here?
Appreciate!
Also, as I understand from debugging, when using paths the package.json is still checked for typings field and if found it's used as a module resolution?
package.json checking logic is dependent on your module resolution strategy. if node they are always checked. foo\index is uses to resolve foo.. etc..
So as I understand the TS compiler treats d.ts modules as first class citizens just like .ts files instead of simply being a complement to .js files?
not sure i understand the question.
I get an error:
Error:(2, 20) TS2306: File '.../node_modules/jsep/typings/tsd.d.ts' is not a module.
path mapping gets the compiler to the file, the import expects the file to be a module.. a module is a file with at least one top-level import or export..
declare module 'jsep' { is a global declaration that adds a module called jsep as a global. to make it a module, just lose the declare module 'jsep' { and let the file end with export = jsep.
@mhegazy, thanks for the clarification!
not sure i understand the question.
I meant here that if, for example, I have myf.d.ts with the following declaration:
export declare function myf(): string;
and if I reference it from main.ts:
import { myf } from "./myf";
the module is successfully resolved even if there's no actual implementation of myf, there's no myf.js.
declare module 'jsep' { is a global declaration that adds a module called jsep as a global. to make it a module, just lose the declare module 'jsep' { and let the file end with export = jsep.
I'm confused, in this answer as I understood you explained that a module with quotes is an ES6 module (external module). I'm looking at the jsep typings file and it correctly declares a module with quotes and there's also an export declaration:
declare module 'jsep' {
...
function jsep(val: string | jsep.Expression): jsep.Expression;
export = jsep;
Also, it's working fine without any modifications if I don't use paths. Any comments on that?
the module is successfully resolved even if there's no actual implementation of myf, there's no myf.js.
The compiler looks for a .ts file, if it did not find it it looks for a .d.ts file, if it did not find it it looks for a .js file.
the existence of the .d.ts file is an indication that you have a module there. I would not put a .d.ts file without a .js file unless you have a post-build step that will make sure the .js file exists at runtime.
I'm confused, in this answer as I understood you explained that a module with quotes is an ES6 module (external module). I'm looking at the jsep typings file and it correctly declares a module with quotes and there's also an export declaration:
This is not about namespaces vs modules. this is about the scope of the declaration, and the result of importing a file..
What i am saying is isntead of:
declare module 'jsep' {
namespace jsep {
export interface Expression {
type: ExpressionType;
}
... ....
}
function jsep(val: string | jsep.Expression): jsep.Expression;
export = jsep;
}
it should be:
namespace jsep {
export interface Expression {
type: ExpressionType;
}
... ....
}
function jsep(val: string | jsep.Expression): jsep.Expression;
export = jsep;
The file is still a module because it has export = at the bottom.
when you define declare module "foo" you are putting it in the global scope, and saying the module name here is "foo". when u use export in the file without declare module you are saying this file is a module, regardless of how you name it.
the later is what path mapping gets the compiler to use, since it changes the name of the module during resolution.
you are saying this file is a module... the later is what path mapping gets the compiler to use, since it changes the name of the module during resolution.
thanks, that's a very important clarification! I understand now why it worked differently with and without using paths. So is there a way to use the same the same typings file definitions in both situations with and without paths? Do I need to configure paths in some specific way?
Now I'm experimenting to try to understand better what you said. I've created the following file:
// myf.d.ts
declare module 'myf' {
function myf(): string;
export = myf;
}
and then try to import it:
// main.ts
import * as myf from './myf';
But I get the error Error:(1, 22) TS2306:File 'D:/playground/typescript-playground/myf.d.ts' is not a module.. If I remove the wrapping part declare module 'myf' from the myf.d.ts I get the other error Error:(1, 22) TS2497:Module '"D:/playground/typescript-playground/myf"' resolves to a non-module entity and cannot be imported using this construct.. What am I missing here?
The documentation would certainly help.
I'm also interested in the difference between the two declarations:
// ES6
export default function myf(): string;
// TS
function myf(): string;
export = myf;
So is there a way to use the same the same typings file definitions in both situations with and without paths
"paths" only works for modules. in this case this is a global script. you do not need path mapping for it. just add it to your project explicitly or through a /// <reference path=".." /> comment.
I'm also interested in the difference between the two declarations:
https://www.typescriptlang.org/docs/handbook/modules.html has docs on that.
The short answer is export default exports a property called "default" on the module object, similar to how export var x exports a property called "x"; whereas export = replaces the whole module object with a new object. the latter does not exist in ES6 module spec, and it only exits to model CommonJS and AMD modules.
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.