[FEATURE] absolute->relative module path transformation
nakamorichi opened this issue Β· 96 comments
Problem
tsc does not support transforming absolute module paths into relative paths for modules located outside node_modules. In client side apps, this is often not an issue because people tend to use Webpack or similar tool for transformations and bundling, but for TypeScript apps targeting Node.js, this is an issue because there is usually no need for complex transformations and bundling, and adding additional build steps and tools on top of tsc only for path transformation is cumbersome.
Example input (es2015 style):
import { myModule } from 'myModuleRoot/a/b/my_module';Example output (CommonJS style):
const myModule = require('./a/b/my_module');My personal opinion is that relative module paths (import { ... } from '../../xxx/yyy';) are an abomination and make it difficult to figure out and change application structure. The possibility of using absolute paths would also be a major benefit of using TypeScript for Node.js apps.
Solution
Compiler options for tsc similar to Webpack's resolve.modules.
Could this be achieved for example with existing baseUrl and paths options and adding a new --rewriteAbsolute option?
Related
My personal opinion is that relative module paths (import { ... } from '../../xxx/yyy';) are an abomination and make it difficult to figure out and change application structure.
While I heartily agree with your sentiment, I think rewriting such imports would open up a can of worms that would ultimately break or complicate an insanely large number of tools and workflows that rely on the emitted JavaScript using the same module specifiers. Even under a flag, I think it will introduce a lot of complexity.
I hate relative paths that go up, it is just awful for maintainability, but my two cents is that this needs to be done on the NodeJS side, not the transpiler side. Of course it's extremely unlikely that will ever happen...
This issue can be solved if add node_modules directory with symlink to src directory:
project_root/
node_modules/ <-- external modules here
src/
node_modules/ <-- keep this folder in git
src -> ../src <-- symlink to src
a/
b/
c.ts
d.ts
tsconfig.json
In this case you can use the following import in c.ts:
import * as d from 'src/d';instead
import * as d from '../../d';All will work in typescript and commonjs. You just need to add exclude field in tsconfig.json:
{
"exclude": [
"src/node_modules"
]
}@aluanhaddad I've been using path rewrite (Webpack) on client-side since I began writing React apps. How would it cause problems on the server side if it doesn't cause problems on the client side? Editors already support setting module roots, and linters (ESLint, TSLint) seem to be fine also.
To the maintainers of TypeScript: Please add support for custom module roots so that we can get rid of the awful and unmaintainable import paths.
@ikokostya Many tools have special treatment for node_modules, and it is always treated as a folder for external code. Putting app code into node_modules may solve the import path problem, but introduces potential other problems. It is also ugly solution that shouldn't, in my opinion, be recommended to anyone.
Many tools have special treatment for node_modules, and it is always treated as a folder for external code.
Could you provide example of such tools? In my example all external code are placed in node_modules in project_root.
Putting app code into node_modules may solve the import path problem, but introduces potential other problems.
Which problems?
It is also ugly solution that shouldn't, in my opinion, be recommended to anyone.
Maybe you should read this
- https://gist.github.com/branneman/8048520
- http://stackoverflow.com/questions/10860244/how-to-make-the-require-in-node-js-to-be-always-relative-to-the-root-folder-of-t#24630974
And why it's ugly? It uses standard way for loading from node_modules.
@Kitanotori perhaps I misunderstood your suggestion, TypeScript supports this feature with the --baseUrl flag.
My point was that NodeJS doesn't support it and that TypeScript should not attempt to provide the future on top of NodeJS by rewriting paths in output code.
Could you provide example of such tools?
There are too many to count but TypeScript is definitely an example of such a tool.
I think putting application code in a node_modules folder is a very ill-advised hack.
@aluanhaddad --baseUrl setting enables to transpile the app, but what's the point of being able to transpile successfully if you can't execute it? ts-node does not seem to support --baseUrl, so I think it is inaccurate to say that TypeScript supports custom absolute paths.
@ikokostya Thanks for the links. It seems that setting NODE_PATH in the startup script is the best way currently. I think I will go with that approach.
@Kitanotori
@aluanhaddad --baseUrl setting enables to transpile the app, but what's the point of being able to transpile successfully if you can't execute it? ts-node does not seem to support --baseUrl, so I think it is inaccurate to say that TypeScript supports custom absolute paths.
The point is that it does execute perfectly in environments that support that. RequireJS, SystemJS, and even Webpack support setting a base URL.
What I'm trying to say is that the issue is on the NodeJS side. TypeScript provides base URL configuration to integrate with and take advantage of those other tools.
Our general take on this is that you should write the import path that works at runtime, and set your TS flags to satisfy the compiler's module resolution step, rather than writing the import that works out-of-the-box for TS and then trying to have some other step "fix" the paths to what works at runtime.
We have about a billion flags that affect module resolutions already (baseUrl, path mapping, rootDir, outDir, etc). Trying to rewrite import paths "correctly" under all these schemes would be an endless nightmare.
@RyanCavanaugh Sorry to hear that. If Webpack team was able to deliver such feature, I thought TypeScript team could also - considering that TypeScript even has major corporate backing. It's a shame that people are forced to add another build step on top of tsc for such a widely needed feature.
Still hope TypeScript could support this.
Or Just make it easy to create a plugin, so we can build something like babel-plugin-module-resolver to make it work. (ref: #11441)
So anyone got any solution or a really nice workaround without babel to make this happen ?
Edit
I've ended up with a custom transform script using gulp & tsify & gulp-typescript
https://gist.github.com/azarus/f369ee2ab0283ba0793b0ccf0e9ec590
Browserify & Gulp samples included.
So anyone got any solution or a really nice workaround without babel to make this happen ?
@azarus I went with the solution of adding this to my top level file:
import * as AppModulePath from 'app-module-path';
AppModulePath.addPath(__dirname);The downside is that if the loading order differs from expected, the app crashes. Better solution would be to have a compiler plugin for converting absolute paths to relative paths. However, I'm not sure if TypeScript has any kind of plugin feature.
I created a post-build script for converting absolute paths to relative (doesn't yet have way to set the module root paths, but one can easily implement such): https://gist.github.com/Kitanotori/86c906b2c4032d4426b750d232a4133b
I was thinking of having the module roots being set in package.json via moduleRoots array containing path strings relative to package.json. I wonder what kind of risks there are in this kind of approach?
I've ended up with my own post build script too,
https://gist.github.com/azarus/f369ee2ab0283ba0793b0ccf0e9ec590
Browserify & Gulp samples included.
It acutally uses the tsconfig.json paths and baseUrl setup :)
I am gonna make a npm package from this during the weekend, i just haven't had time to properly test the script.
I have this in my tsconfig:
{
"compilerOptions": {
...
"module": "commonjs",
"moduleResolution": "node",
"rootDir": "src",
"baseUrl": "src",
"paths": {
"~/*": ["*"]
}
},
I can't remember if the other options above are necessary, but "paths" will allow this:
import {fundManagersApi} from "~/core/api/fund-managers.api";
The core directory is at src/core.
And then I use the npm package "module-alias" and do:
require('module-alias').addAlias("~", __dirname + "/src");
At the top of my top level file.
Webstorm understands the paths option and will auto-import correctly even.
I use tsconfig-paths to handle this during runtime. I'm not aware of any performance implications or issues otherwise. The one objection I could see is monkey-patching Node's module resolution strategy.
@kayjtea I don't think module and moduleResolution are related. Here's a slightly simpeler version that seems to work:
"baseUrl": "./",
"paths": {
"~/*": [
"src/*"
]
},Personally, I prefer writing @src, and for this you can use:
"baseUrl": "./",
"paths": {
"@src/*": [
"src/*"
]
},In VSCode I don't seem to have to do anything with my top-level files or npm module-alias.
--- edit ---
Ah damn it. I see now that the paths don't work at runtime work if you don't do more then make the compiler and VSCode happy.
@azarus I think I might have to go that route too.
I have two repo's that are also depending on roughly the same set of helpers and services. And I am getting tired of copying code between the two.
@0x80 i ran into a similar problem when had to share model definitions between 5 different services. I solved this by creating a shared folder that everyone were able to access if needed.
Setting up an NPM link is also easy and less hassle than you would think.
But i am sad that proper solution is still not in place.
Use tspath (https://www.npmjs.com/package/tspath) run it in the project folder after compile, no config needed, it uses the tsconfig to figure things out (a recent version of node is needed)
In case you want to configure create-react-app project add:
{
"compilerOptions": {
//... other react-scripts-ts options
"paths": {
"src/*": ["*"]
},
"baseUrl": "src"
},
Is equivalent to NODE_PATH=./ in .env in standard c-r-a project.
I think that this feature request should be re-considered since es6 modules are now on browsers and browsers do not know what to do with non-relative paths. Somehow the non-relative paths should be replaced with relative paths.
I agree. But If i recal this feature request was declined because "its out of the scope of the project" "use something else"... "we already have a solution in place" (and that solution sucks btw).... and many other nonsense reasons. Would be much easier for everyone if ts would translate project absolute paths to relative paths. If the reasoning "many developers need this out of the box" is aint enough.. why is typescript still in development then?
I have found module-alias or tsmodule-alias to be useful in these situations. It does, however, make sense to me to support absolute to relative path mapping in Typescript.
If they don't want to implement its another reason to have TSC plugin system so we can integrate such functionality on our own directly to TSC build process with possibility to use tsconfig, ast, file lists, whatever is tsc using internally without need of another bunch of external, post processing tools.
@RyanCavanaugh I'm not sure how you can consider it "out of scope" to fix a half-baked feature that:
- results only in broken/unusable output
- contradicts well established precedent (webpack, etc)
- is contrary to expected behaviour for a significant number of people
- produces an arbitrary and erroneous failure
That seems to meet even the most forgiving definition of "broken".
Typescript already (potentially) outputs significantly different code than its input, so a tiny bit more does not seem incongruent with existing functionality.
It's a bit ridiculous to expect adding webpack solely to work around this obviously undesirable behaviour: You claim that the purpose of paths is for satisfying the compiler; but then what is the value of this compiler check/error at all? Nothing, because making the compiler happy results only in a happy compiler but broken output, and producing unbroken output is its main job.
this needs to be done on the NodeJS side, not the transpiler side. Of course it's extremely unlikely that will ever happen...
Shirking responsibility to the VM as @aluanhaddad suggests is not a solution; especially when the suggester already knows that is not a reasonable expectation (because it's not the VM's responsibility to resolve bundle issues).
This should be re-classified as a bug instead of a feature request: Asking for a feature to not break is not a new feature or an enhancement. Otherwise, you might as well add a warning:
paths
β οΈ Using this feature on its own will break your project. It handles only half the job (and not the useful half); it must be paired with a bundler like webpack to handle the missing half.
In terms of complexity, is this not a find and replace that is aware of values (baseUrl, outDir, etc) that TypeScript is already using (and then dropping on the floor)? Monkey-patching that (with some kind of post-processor) would be a significantly more difficult implementation than doing it right originally.
This definitely needs to be re-opened and roadmapped. It's causing no end of hell for my team and I.
I'm all in for at least improving documentation about lack of a runtime support for this option. Using paths for anything other than .d.ts files is a great way to waste time being confused why the code is happily compiling, but not working.
The fact that this isn't documented resulted in a massive time waste for me.
I agreee with @jshado1 at around 90%.
I don't think it should be a note, I think it should be fixed. It's not even specific to something. It's a functionality to help developers manage large projects.
It's like adding syntactic sugar in C#, but generated code is not valid unless you use something else to fix the generated output.
It should either be removed and leave us with nothing, or fix it so it generates valid code regardless of other tools.
It's not even specific to node, there should be a flag transforming relative paths to absolute paths. It's a functionality for helping developers manage large projects.
The funny thing is typescript has flags specific for 'react' so it is already are doing something for third party library. A flag to generate valid builds when using aliases is "out of scope" even if it's a generic functionality.
What happens if in 3 year's there is a cool new tool that we will use and it does not support relative paths.
What happens if we don't want to use a another tool.
What happens if webpack for some reason drops relative paths.
Please stop using excuses as "It's a Node problem" or "TSC is not a build system, use Webpack", because this is indeed a Typescript problem that is annoying plenty of people, and TSC is indeed a build system, now there is even a --build parameter in 3.x.
It is a can of worms? Yes
It is a must to fix? Also yes
My first impression of TypeScript is banging my head against this wall for 2 days. Not fun π©
This seriously needs to be fixed.
With our current set up we're using a babel-plugin-module-resolver on our shared library code after our Typescript build completes in order to fix the module paths. Unfortunately, Babel doesn't compile type files, so we have to use relative paths anytime we reference a type in our library which is leading to horrible paths like ../../../../../ui/type.d.ts.
Well, if you dont want us to use absolute paths, then you should not introduce baseUrl config, because its not make any sense if my application wont run in real world when i use absolute paths. And having a config named baseUrl is really misleading. Its wasting developers time to finding out what the problem is.
So your general take on this is simply wrong.
Well, there are already more than 5 issues with 100+ responses voting to fix this with at least 1 new comment per-week. Seems like TypeScript devs simply avoid the problem pretending it does not exist.
I think this may never be resolved until somebody with a PR comes and solves this "Too Complex" and "Out of Scope" problem for them.
That being said, the possible PR also has to be accepted for this to be fixed though. Which does not seem to me like much of a possibility given the conservative mindset of maintainers in regards to this problem.
P.S. For anyone who got here looking for a solution (like I did), tsconfig-paths seems to be the most convenient hack to make this work without adding webpack or babel to your dependencies.
Also, as @MRazvan stated:
The funny thing is typescript has flags specific for 'react' so it is already are doing something for third party library.
@RyanCavanaugh, this line alone breaks any argument about this issue being "Out of Scope" as TypeScript already does waaay more than its "scope" should allow it to do.
Current list of such things includes but not limited to:
--reactNamespace- Specifies the object invoked for createElement and __spread when targeting "react" JSX emit. This option got me thinking stuff;--build- Builds this project and all of its dependencies. Should not be there according to the logic of "not in the responsibilities of a compiler";--jsx- Based on the aforementioned "compiler scope logic" should also be left to other compilers to deal with (why make exceptions for react? why not add vue-templates compilation then too?) and should be replaced with just--preserveJsx=true/false;
So, whadda we do with all that?..
If resolving relative paths is "Out of Scope" then things like building other projects, compiling single-front-end-framework-specific syntax and making exceptions for single (even though quite popular) frameworks should be not only "Out of Scope" but even out of consideration for being implemented.
Speaking of this:
rather than writing the import that works out-of-the-box for TS and then trying to have some other step "fix" the paths to what works at runtime
- Custom import paths do not "work" for TS out-of-the-box. Current way of making this work in TS is "some other step to fix the paths" because they didn't work in TS-compile-time. So, the "paths" options is already a hack. Though, it's a hack that only cares about TypeScript being happy and not anyone else. That being said, by making custom paths render into es-compliant relative paths would not be a breaking change too, since all the other transpiling tools like webpack or rollup do not care about relative paths being relative anyway.
- Replacement of custom paths with TS-resolved relative paths can be done at the same step as the TS resolves them due to TS compiler being "aware" of the paths in question right at the time the paths need to be changed with all variables and flags in mind. Not everything in later cycles of software development has to be the "some other step to fix" that is duct-taped to the code somewhere.
That being said, I would gladly propose a PR myself, as I study the code at this exact time. But the lack of reliable documentation and messy code structure make this process extremely painful, so I have no idea of how long it can take me to propose any reliable enough solution.
You can compile absolute and path-based TypeScript imports to relative Javascript files using ts-transformer-imports and ttypescript
I've created https://github.com/joonhocho/tscpaths and have been using it for my projects.
It replaces absolute paths to relative paths in compile time. (tsconfig-paths does it in run-time).
You can simply add it to your build script after tsc and that's it.
This is not a mature project, so it may not work for your if your setup is complicated, but I've been using it fine for my setups without any problems.
PRs are welcome!
Thanks to @joonhocho's module I was at last able to use typescript with nexe, which bundles a node project into a single executable file.
In such a setup, runtime modifications to the path were ineffective.
We had the same problem at Dropbox and open-sourced this transformer https://github.com/dropbox/ts-transform-import-path-rewrite
Just to throw this out there we've had the same problem which drove me to create this package yesterday https://github.com/kvendrik/ts-absolute-paths-transformer. @longlho's solution looks great as well so whichever works best for your project. π
For those interested, here's an SO answer on how to use webpack in your nodejs project to enable absolute imports.
disclaimer: my answer.
Typescript generates typings, webpack does packing. Typings has imports with aliases. They are deeply in crap.
It's very frustrating that this remains such a huge problem. The argument that TS shouldn't be responsible for fixing path aliases is baffling. What's the point of allowing path aliases if the compiled output is broken as a result of using them? Please consider adding a flag to allow alias fixing, or provide a simple plugin hook so that folks who need this have some option to deal with it (like ttypescript does).
After struggling with this for a while, and finding the best solutions I could for our projects, I wrote up my notes.
I'll duplicate them here, in case anyone can correct my mistakes, or benefit from the summary.
Module Resolution and Import Paths
TypeScript supports relative and non-relative import paths.
See: https://www.typescriptlang.org/docs/handbook/module-resolution.html
Non-relative paths work immediately for dependencies from external packages. But without further configuration, only relative paths work when importing from files in the local project. Primarily due to upward navigations, relative paths quickly become difficult to understand and maintain, once the code is organized into directories more than one level deep.
import { Banana } from '../../../model/fruit/Banana';
TypeScript's baseurl / paths feature partially addresses this problem.
See: https://www.typescriptlang.org/docs/handbook/module-resolution.html#base-url
A simple configuration of baseurl and paths can allow the above import to be written without the upward navigations.
import { Banana } from 'src/model/fruit/Banana';
This is part of a fine solution, but creates new problems downstream. While the TypeScript compiler is capable of finding the dependencies using baseurl and paths, and completing the compilation successfully, the compiler produces JavaScript files which contain the original import paths as written in the TypeScript source. Typical consumers of those JavaScript files, including Node.js, browsers, and critically, other projects referencing the library, will fail to resolve those imports, because those consumers are unaware of the original baseurl and paths scheme.
This is, by assertion of the TypeScript team, not a bug or pending enhancement in TypeScript. The TypeScript team states clearly that adjusting the paths in the output JavaScript to reflect the compilation context is out of scope. Instead, post-compilation consumers must understand the original paths. In practice, that means either writing ugly relative paths, or re-interpreting baseurl and paths, or their equivalents, post-compilation.
See: #15479 (comment)
Many solutions to this problem have been published. Here are the few I've selected to use.
First: tsconfig-paths
https://www.npmjs.com/package/tsconfig-paths
This hijacks the module lookup calls in Node.js and fixes the paths on the fly at runtime. It's heavily used, and works well for in-memory tasks, like test frameworks. But it doesn't help browser runtimes or other projects referencing the library, because it doesn't write out adjusted JavaScript files to be consumed later.
Second: tsconfig-paths-webpack-plugin
https://www.npmjs.com/package/tsconfig-paths-webpack-plugin
This helps webpack resolve imports. It helps solve the problem for browser consumers.
Third: @zerollup/ts-transform-paths
https://www.npmjs.com/package/@zerollup/ts-transform-paths
This rewrites the import paths to working relative paths in the output JavaScript during TypeScript's compilation process. This helps solve the problem for other projects referencing the library. Like tsconfig-paths, this can be used with ts-node to get test frameworks to run, so they're partially redundant for that purpose. You'll find npm scripts illustrating both approaches in these projects.
Here are a handful of other options I found, but ultimately rejected in preference for the above:
https://www.npmjs.com/package/ts-transformer-imports
https://www.npmjs.com/package/ts-absolute-paths-transformer
https://www.npmjs.com/package/tscpaths
https://www.npmjs.com/package/ts-transform-import-path-rewrite
https://www.npmjs.com/package/tspath
https://www.npmjs.com/package/ts-transform-paths
https://www.npmjs.com/package/babel-plugin-module-resolver
Other references:
Are there any working solution to make path aliases work for type declaration files? I was trying to use TypeScript to write a reusable library, however the path won't be resolved in the generated *.d.ts file. I could use 3rd party tools to resolve the generated *.js code, but they don't work with type declaration files.
We wrote one for that use case in Dropbox https://github.com/dropbox/ts-transform-import-path-rewrite
This solution works for output javascript and type definition files:
https://www.npmjs.com/package/@zerollup/ts-transform-paths
@longlho @craigsumner none of them is working... at least I couldn't configure them to work. any chance you link to a small repo for use example with ttypescript?
@stavalfi I can confirm that @craigsumner's suggestion works, we've been successfully using it in production for months.
It's been a while since I set it up but looking at my configuration files it looks like this is what you need to do:
First install @zerollup/ts-transform-paths and ttypescript as your NPM/Yarn dependencies. Next update your tsconfig.json file to include the plugins section. This is how mine looks like:
{
"extends": "./tsconfig.json",
"include": ["src/**/*"],
"compilerOptions": {
"baseUrl": ".",
"removeComments": true,
"paths": {
"lib": ["src/lib"],
"lib/*": ["src/lib/*"],
"types": ["src/types"],
"types/*": ["src/types/*"]
},
"plugins": [{ "transform": "@zerollup/ts-transform-paths" }]
}
}You probably don't need the paths but in case it doesn't work without them, notice that I had to include each path twice - once for the root directory and once for all its contents.
Finally, update your package.json to use ttsc. I've improved with additional steps and it looks like this but you can obviously just point build at the ttsc command:
"scripts": {
"build": "yarn build:clean && yarn build:compile && yarn build:assets",
"build:assets": "copyfiles \"./src/**/assets/**/*\" ./dist --up 1",
"build:clean": "rimraf dist",
"build:compile": "ttsc -p tsconfig.prod.json"
}We have separate configuration files for development and production hence I'm referencing tsconfig.prod.json. In your case it would probably be just ttsc without any parameters.
If any typescript library creator still has this problem, I created a Webpack loader for babel-plugin-module-resolver, which converts absolute paths to relative paths:
https://github.com/stavalfi/babel-plugin-module-resolver-loader
It also solved the problem of .d.ts files with absolute paths by converting them to relative paths.
What do you think?
It gets even more shaky, when you have .d.ts files in your source directory that contain absolute path aliases and are exposed to client projects (in my case they were needed by the main types file for the package). π
I found tscpaths (this one) to be useful for the task, as it converts paths of a) emitted js files b) emitted declaration files c) your source declaration files, if you copy them over to your build dir first. It is also just a compile time dependency. Good job!
PS: I randomly came across an example, where tsc compiler itself actually resolves path aliases to relative paths in the emitted d.ts files.
Take this repo for example: https://github.com/ford04/tsc-declaration-files-abs-path-sample
After running npm i && npm run build, you can see that the code from src/index.ts
import { MyType } from "my-lib/types";
...
const doSomething = (persist: MyType) => {
return 42;
};
is emitted in dist/index.d.ts in form of
declare const doSomething: (persist: import("./types").Persistence<import("./other-types").DateType, import("./other-types").DateType>) => number;
So there is apparently already some functionality out there for the compiler that actually does path conversions? It seems to be emitted like this, when there is a transitive dependency that spans over multiple files, but I am not sure.
Could you explain, why this resolve mechanism cannot be used for all ts and d.ts files?
Greetings
ford04
How is this still a thing? Obviously someone should do something about in the TypeScript tooling itself.
@RyanCavanaugh, please give us some hope, youβve been always so kind in other discussions. Youβve been keeping silence about this particular topic for 2 years since your last answer. Hundreds (if not thousands or even tens of thousands) people spend hours and days on hacky work-arounds and experimenTS. I feel that by October 2019 nothing in TS missing features resonates more than the topic about rewriting the absolute imports.
Please!
Honestly, this issue is the main reason I've moved my department off of TypeScript, and mostly due to the TypeScript team's arrogant "eff you, community" attitude. Just more classic Microsoft π
@jshado1 I have also moved away from TypeScript - and JavaScript - on backend. There are good alternatives, such as Java/Kotlin (*have come to like Spring Boot in particular), Go, and even Rust. With the current module import syntax and the quirks related to it (CommonJS vs ES2015, etc.), I just can't consider TypeScript/Node.js as a serious candidate for large-scale backend apps (had particularly high hopes for LoopBack..)
@dko-slapdash we don't do things that disagree with our core design goals, which this does. It doesn't matter how many people ask us to do something that we think will make TypeScript actively worse.
@jshado1 honestly, that hurts. I try to respond to everyone here as a real human, and we have to have the ability to make technical decisions, of which this is one.
@RyanCavanaugh makes total sense! As a suggestion maybe you could point out out what design goals this violates and why so everyone is clear exactly on why this is not being implemented? Then, I'm also thinking it might help to also post a list of methods people can use to fix this issue so it's not a blocker for consumers. Or, if this is not something Typescript is supposed to do itself, if there's value in it and there's enough demand, maybe there should be an officially supported adapter/middleware that addresses this issue? What do you think?
@RyanCavanaugh, thanks for the explanation. We wonder though, how adding an optional flag to compiler options (which would immediately be utilized by thousands of engineers) could make TypeScript worse, especially actively worse. Considering lots of other flags changing the compiler output (--reactNamespace, --build, --jsx) and features (like βdefine class properties in constructor parametersβ) already exist. Where is the boundary?
Design goal number 7 says
Preserve runtime behavior of all JavaScript code.
When you write the JavaScript line
var n = 10;
TypeScript says "This is JavaScript code" and will always emit
var n = 10;
When you write JavaScript code, TypeScript says that you wrote JavaScript code, and the behavior of this is preserved.
When you write JavaScript code, TypeScript doesn't change the behavior of it.
This is critical, because it means you can take existing JS code, move it to a TS file, and be guaranteed that the code will, at runtime, behave the same way, no matter what your config file says, no matter what the code does.
This allows you to make confident predictions about how your code will run, which is the most important thing!
This is why we don't have any of these extremely-requested features, even under a commandline flag:
- Partial classes
- Extension methods
- Type-based overloading dispatch
- Runtime exceptions on divide-by-zero
- Operator overloading
- Automatic resolution of class members that should be accessed via
this. - Typed 'catch' clauses
In the 7 years I've been doing this, we've always said that we don't do features that change the behavior of JS code, and people have been telling me that TS will fail unless we add a commandline flag that sometimes changes the behavior of their JS code. Maybe we have failed, I don't know, but we're going to keep going on this path either way.
import x from "./y";This is a line of JavaScript code. When you write JavaScript code, TypeScript doesn't change the behavior of it. Even under a commandline flag!
For this feature in particular, our message has consistently been: Just like for all other things, you should write the JS the way it should be at runtime, and tell TS how to interpret that. That's why we have eleventyhundred different commandline flags for controlling module resolution and zero for controlling module path rewriting. It's a straightforward principle that aligns with every other way we address JS code.
@RyanCavanaugh why is this: import x from "./y"; a JavaScript code? What would be proper TypeScript version of this I wonder?
why is this:
import x from "./y";a JavaScript code?
It's every bit as much JavaScript code as console.log("hello, world"); is
@RyanCavanaugh, that explains a lot! Ok, so there will be no command-line flag which changes the behavior of a valid (!) JS code, thatβs clear. If some code is a clear JS code, itβs never changed at compile-time; if some code is not (like tsx or properties defined in constructor arguments), it may be changed at compile-time and translated to some other JS code.
What about plugins which are able to change the generated code though? People currently struggle with ttypescript to support them (but itβs pretty ugly, since everyone has to run ttsc instead of tsc and tts-node instead of ts-node). I mean - is it in a roadmap, to allow people create native TS plugins (without ttypescript crutches) which affect code generation?
Plugins are under discussion; see #16607
This is great @RyanCavanaugh thank you for writing that up! For anyone who needs a absolute->relative module path transformation right now, what would you recommend? Should they keep using what they're using now and then if Compiler Plugins become a thing there is a chance for someone to write a possibly cleaner alternative? Do you ever see a scenario where Microsoft will start officially supporting some of these plugins to provide people with those highly-requested features or will you likely leave that to the community?
I am encountering this issue and, like others have suggested, using a post tsc build script to correct the import statements in the compiled JavaScript and TypeScript definition files.
According to the Module Resolution section of the documentation (as of time of writing):
The TypeScript compiler supports the declaration of such mappings using
"paths"property intsconfig.jsonfiles
Indeed, given the following project layout ...
projectRoot
βββ src
β βββ file1.ts (import * as file2 from 'file2')
β βββ file2.ts
βββ tsconfig.json
... and the following tsconfig.json ...
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"src/*",
]
}
}
}
... tsc --noEmit will succeed. Additionally, there will be no errors in VSCode.
However, if you compile the project and attempt to run your app, or if this is a library and you attempt to consume it, you will encounter a runtime error.
I understand the design goals as explained by @RyanCavanaugh, but is the documentation wrong? If supporting absolute paths is against the design goals, why support it during type checking, but not during compilation? If absolute path resolution is really against the design goals, the "paths" feature should be removed completely. It would save a lot of headaches and be less confusing to those migrating their codebases to TypeScript.
I hate relative paths that go up, it is just awful for maintainability, but my two cents is that this needs to be done on the NodeJS side, not the transpiler side. Of course it's extremely unlikely that will ever happen...
true when you refer to the javascript output... but definitely not true when we discuss the types output that our package exposes..
Why are you dooming us to develop infra packages with relative references instead of aliases??
Why not making it a flag for the compiler.. it is a basic find and replace!!
I can't believe this remains such a common outstanding problem.
The compiler is turning working TypeScript into failing JavaScript, all while claiming "no errors".
@RyanCavanaugh stated the following:
This is critical, because it means you can take existing JS code, move it to a TS file, and be guaranteed that the code will, at runtime, behave the same way, no matter what your config file says, no matter what the code does.
This allows you to make confident predictions about how your code will run, which is the most important thing!
I agree entirely that the most important thing is the ability to make confident predictions about how the code will run. As it stands, if you use the baseUrl/paths feature, this is not an ability you can be sure of.
As evidenced by years and years of comments, in this Issue and several others, from people running into exactly this issue while trying to fix horrific relative "../../../../" file paths: the compiled code simply does not behave as any reasonable person would expect, and renders the paths feature completely obsolete for what is advertised as its explicit use case.
Did we have any progress on that? Coming from another language and just starting using this tooling, I find it strange that it is not already supported (with a least a flag).
I fixed it using this library tsc-alias
"script": {
"build": "tsc && npx tsc-alias"
}Ex: from @src/controllers/user.ts to ../controllers/user.js
after years of fighting this issue and trying endless plugins (including writing my own plugin), i gave up.
the eco system doesn't like non-relative paths. not just typescript.
setting up typescript, webpack,babel,jest,eslint is just the start.
if you came to this issue for the first time, do your self a favor and go back to relative paths. they aren't that bad. if you are working in a monorepo, you can setup jest-aliases with typescript-aliases to point to other packages by the name of the package instead of the relative path to it. but no more than that. (examples can be found on my node-typescript-monorepo projects)
@stavalfi I feel your argument supports the idea of having the compiler do it for you. If you use an alias, there is no reason why it cant transform the path to relative at build time. Even if that was optional I just don't understand why this is a not fix.
The way how module resolution works in Node (and in TS as a consequence) is a fundamentally leaking abstraction. I mean, itβs not too bad, but itβs limiting and leaky. Itβs the entire ecosystem, not just TS. E.g. in package.jsom, we can specify the main script mapping, but canβt specify an entire root folder mapping, so module authors have to include all sub-files/classes in index.js (or main.js or whatever) instead of exposing the entire folder in Java-alike style. (This is whatβs changing slowly BTW.) Another example is a zoo of module formats and ES6 modules (.mjs and that itβs not an optional filename extension).
TS also has its own abstractions and dogmas on top of this. And within the TSβes set of abstractions/dogmas, itβs perfectly correct to not support paths translation. But all the abstractions in the world are leaky and start limiting the users at some point; achieving something within the frame of reference of those abstractions becomes hacky and hard.
Also, I think someone in the TS team has a very strong opinion on not making an exception in the case of path translations. Despite TS itself is basically a collection of quirks, exceptions and trade-offs (and this is what brings it the most of its value actually).
Wasted hours on this but it's probably because I'm new to Typescript. Still glad I found this thread so I can stop trying. Reverted back to relative path.
Update: I now use this feature alongside webpack config.
I think, if tsc not gonna resolve these paths in a meaningful way(even with an option in tsconfig.json or a flag), it's not useful as the user intends to resolve this path by typescript itself.
If we use another tool to resolve modules as a post-build step, there is no useful outcome from tsc. You gotta do additional work. I wanted to use path aliases and now I will not use it at all.
Braindead decision.
I fixed it using this library tsc-alias
"script": { "build": "tsc && npx tsc-alias" }Ex: from
@src/controllers/user.tsto../controllers/user.js
It saved me
@RyanCavanaugh do you think eslint-plugin-import should be responsible for adding a rule that could ensure the developper uses ".js" extension for every relative import?
It was a bit annoying but I finally managed to work this out with my Rollup config. I wrote about it in StackOverflow but what I basically did was add @zerollup/ts-transform-paths
It was a bit annoying but I finally managed to work this out with my Rollup config. I wrote about it in StackOverflow but what I basically did was add @zerollup/ts-transform-paths
@TeemuKoivisto Thank you so much, it worked great with a rollup config. Now I have all boxes checked: clean absolute paths, vscode that understands them (absolute auto-imports) and correct definitions for external use.
I just don't understand why this isn't a thing. TypeScript added support for the paths and then they're nearly completely useless for normal runtime, why? The fact that it's here, doesn't error at compile-time, and then errors at runtime, doesn't make any sense, it should be translating the paths to actually run like it says it will when defining these paths. Even if it's just an option, the lack of the ability to do this directly within tsc makes the entire paths option useless and even misleading.
Another issue is when you use typescript with the msbuild typescript package.
I don't have NPM, I don't use NPM build pipeline, I don't use webpack.
I just want the output javascript files to append the base url (as an option).
What is the argument against this setting in compilerOptions?:
"appendBaseUrl" : true or false
This would allow us to use relative paths just fine.
import { Something } from "global.js";
Would become:
import { Something } from "./global.js";
With these options in tsconfig:
"compilerOptions": { "baseUrl": "./", "appendBaseUrl": true }
So I find this issue interesting annoying,
I do understand the concept of not modifying the JS but.:
When I have a target with es5, you transpile every await/async, import etc to the promise/require, the generated code does not look at all like my .ts files.
Yet on this one to just add the extension at the end of the import, seems to violate the typescript principle.
I must admit I am confused
For anyone who may concern this:
I've found a custom transformer: LeDDGroup/typescript-transform-paths
We can load it with ts-loader or awesome-typescript-loader by wrting such config:
const createTsTransformPaths = require('typescript-transform-paths').default;
{
test: /\.tsx?$/,
use: "ts-loader",
use: {
loader: "ts-loader",
options: {
getCustomTransformers: (program) => ({
before: [createTsTransformPaths(program, {})],
afterDeclarations: [createTsTransformPaths(program, {
afterDeclarations: true
})]
})
}
},
exclude: /node_modules/
}Design goal number 7 says
Preserve runtime behavior of all JavaScript code.
When you write the JavaScript line
var n = 10;TypeScript says "This is JavaScript code" and will always emit
var n = 10;When you write JavaScript code, TypeScript says that you wrote JavaScript code, and the behavior of this is preserved.
I'm quoting this comment here because I come from this other issue
I know the issue discussed here is not exactly about what I'm going to say but here it goes:
Look at this typical import statement:
import MyClass from '../utils/MyClass'
Is that javascript code? Well, yes it is. But the problem here is that each language will interpret this line differently!
And when you transpile code shouldn't it be in a way so the transpiled code is interpreted exactly in the same way the original code was?
It's not about changing JavaScript semantics but rather aligning TypeScript's behavior with JavaScript to ensure consistent interpretation.
I don't know, maybe the followingTypescript dev team principle should be reconsidered.
We are not going to implement features, even under a commandline flag, that imply changing JS semantics by rewriting it during emit
It's not about changing JavaScript semantics but rather aligning TypeScript's behavior with JavaScript to ensure consistent interpretation.
This is exactly why we don't modify the paths - so that it's never a potential source of disagreement between you, TypeScript, and the runtime.
The new module documentation covers this in exhaustive detail and I'd recommend reading it.
I don't know, maybe the followingTypescript dev team principle should be reconsidered.
The problem here is not that we haven't thought about it over the hundreds of comments we've posted and thousands of comments we've read.
@RyanCavanaugh If your goal is to stick to the JS standards, then please do that.
This means you should by default assume any import is referring to an es6 module, not a node module, and treat it as such.
I get that when typescript came out we didn't have modules outside of node, but we do now.
As it stands, by not wanting to disagree with the runtime, you mean "do not disagree with the node runtime".
We NEED the ability to tell the compiler we are running in a node or es environment, as the way imports are handled in those two environments are fundamentally different.
I don't know if anyone looks at old, closed issues, but I suppose this goes here.
In my ignorance of this controversy, I implemented a flag to rewrite the paths (#60723) after many, increasingly dodgy, attempts at recommended workarounds. The glorious feeling of finally being able to stop fighting vscode and tsc was very short lived when I became aware of just how adamantly forbidden it was.
I thought my use case must be very unusual, because of course tsc should be doing this, but this thread has disabused me.
The dogma is that JS must not be modified; but at least in my scenario JS requires relative paths for everything, and I want the JS to be modified. I suppose I could preprocess the TS to fix the paths, or I could postprocess the JS, but the tsconfig paths and rootUri already do exactly what I want - but then it skips the final logical step and throws the information away.
I know how long it took me to try and get around this apparent oversight (before giving up), and we can multiply that by the many thousands of other people facing the same situation. I have read the arguments against modifying the paths and I'm sure there are cases where they make sense, but this is not one of them. Typescript exists to improve the experience and safety of developing javascript, and enabling non-relative paths would absolutely add to that mission.
I know how long it took me to try and get around this apparent oversight (before giving up), and we can multiply that by the many thousands of other people facing the same situation. I have read the arguments against modifying the paths and I'm sure there are cases where they make sense, but this is not one of them. Typescript exists to improve the experience and safety of developing javascript, and enabling non-relative paths would absolutely add to that mission.
Welcome to the fight ha.
Sadly I don't see them moving on this any time soon. I ended up integrating vite into my .net build runtime to get around this issue.
Is it ok that their philosophy is to force us to these measures? No. Will it change? Probably not.
@RacerDelux at least we have a support group
"I am Adrian, and I tried to use non-relative paths in typescript"
I'm going to use my forked version for now because I can be stubborn too!
For everyone who met this, I recommand to use https://github.com/LeDDGroup/typescript-transform-paths, which seems more friendly to maintainers than making a fork directly.