TypeScript
TypeScript copied to clipboard
allow voluntary .ts suffix for import paths
Search Terms
.ts suffix imports extension
Suggestion
Typescript doesn't recognize file imports with .ts suffix.
Allow voluntary .ts to be added to import paths.
Use Cases
It seems right to be able to use a correct path to a file without magic resolution. This would help to align with deno which uses mandatory suffixes o files.
Examples
let
import a from "path/to/a.ts"
behave the same as
import a from "path/to/a"
Checklist
My suggestion meets these guidelines:
- [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- [x] This feature would agree with the rest of TypeScript's Design Goals.
After some more research I wonder if that behavior could even be more guided by a compiler option like --noImplicitSuffix.
That would be in alignment with --noImplicitAny and give the developer the choice, yet push for a valid, not "magical" resolved path if not set.
What are your thoughts on that?
#35148
#35148
Read through the above and it seems to boil down to multiple build targets with different file extensions throws a wrench in things when supporting using the file extensions on imports. Where it diverges in my mind, and hopefully @Jack-Works you can illuminate this a bit, is that we'd be importing .ts extensions which would have to be rewritten anyways.
So... where are we actually at with this?
Scattered issues / feature requests don't help, and the confusing (and somewhat untimely) response(s) from official TypeScript members haven't helped me understand how this feature will be implemented (it will have to be, eventually).
All I can ascertain is that this is has yet to be resolved.
Can someone explain in plain English when and how these multiple issues will be fixed, please?
+1
If I turn on resolveJsonModule and put a MatchmakingConfig.ts and a MatchmakingConfig.json in the same path, a nasty unexpected behavior awaits:
// this results in MatchmakingConfig.json being imported, not MatchmakingConfig.ts!
import MatchmakingConfig from "./path/to/MatchmakingConfig";
Allowing .ts suffix will let us specify .ts files specifically.
I would love to see this issue resolved. Without the support for imports with a .ts extension, it's literally impossible to create a repository that works with Deno, has TS language services and is buildable with tsc. It seems like an arbitrary limitation on the TypeScript side 😕️
@m93a I am using esbuild for transpilation/bundling and the resulting bundle then works in both deno and node. It's a workaround but at least it works.
This would also help for things like https://github.com/import-js/eslint-plugin-import/issues/2111#issuecomment-915663474. Right now eslint-plugin-import thinks that TypeScript files should only import .ts files or omit the extension.
Typescript doesn't recognize file imports with
.tssuffix. Allow voluntary.tsto be added to import paths.
JavaScript doesn't differentiate between file extensions. It simply doesn't care. TypeScript should do the same: simply accept any file, and use it's MIME type instead.
This is pretty crazy with Deno:
import { Foo } from "./types.ts";-> Runs fine but VS Code shows a syntax error:An import path cannot end with a '.ts' extension. Consider importing './types' instead.ts(2691)
import { Foo } from "./types";-> Does not run but VS Code is happy:error: Cannot resolve module /my/path/to/definitely/existing/types
@cawoodm do you have the Deno extension for VS Code enabled? https://deno.land/[email protected]/vscode_deno
My understanding is that the IDE experience with Deno is pretty well solved, so I’d like to understand what exactly the scenarios are that need work.
it's literally impossible to create a repository that works with Deno, has TS language services and is buildable with
tsc
@m93a when you say “buildable with tsc,” what exactly are you looking for? Type checking with noEmit? Does the deno CLI not have something for that?
Deno isn't the only platform where this is an issue, building TypeScript projects with other bundlers like Babel or Metro for React Native also are problematic. Yes, in those instances one can omit the extension entirely, but many of us do not want to.
The primary problem is that this just seems so needless, the build works fine with the .ts extension and TypeScript knows the source files are there, it just refuses to look at them. None of us want any additional behavior, we would just like a way to opt out of the error and for TypeScript to parse the source files anyway so that the LSP server will work.
I think you’re right that this would be fairly easy from an implementation standpoint, but there are two main reasons why we don’t want to move forward without careful consideration of the full problem space:
- If we simply add a flag that allows module resolution to work with
.tsfiles, it will 100% contribute to the misconception that TypeScript does, can, or should emit JS files with the transformationimport "./foo.ts"→import "./foo.js". We already have angry mobs demanding that this happen forimport "./foo"→import "./foo.js"so if we allowed.tsextensions on there without it being very clear what use cases it is intended to serve, it would just pour fuel on that fire. - Such a flag would look like a gift to Deno users, but it would actually be woefully incomplete for them. There are obviously a lot of other parts of module resolution that are either specific to Deno or shared between Deno and the browser that we don’t currently support, and I think those users would be confused and disappointed that we stopped so far short of proper support for these things.
Don’t get me wrong, I want us to solve all of these problems, but we’re still in the phase where we’re trying to develop a thorough understanding of all the different reasons people want this and what the existing solutions are.
I’m not convinced that node12/nodenext are appropriate module resolution modes if you’re writing for the browser or for a bundler.
What about for isomorphic server-side-rendered code?
That’s a good question. You would clearly need to write that code using the lowest common denominator of resolution features supported by all of your targets. The combinatorics of this mean that such a mode will probably never be supported by TypeScript, so you’d have two options:
- Use a single TypeScript module resolution mode that supports at least all of those features and use caution and/or third-party tooling to ensure you don’t use the features that won’t work in all targets.
- If there is a good TypeScript module resolution mode for each of your targets, check the same code multiple times, once per target module resolution mode, to ensure it will work in each target.
There are some combinations of targets that seem to have no possible overlap. For example, if you want to write a relative import path to a TS file in Deno, you need to include the .ts extension. But if you want to compile that same code to work in the browser, you might be stuck, at least if you use tsc alone, as our current thinking is that if we let you write .ts file extensions we would force you to compile with --noEmit. Of course, there are a lot of other tools that could come into play and help you out here. Vite seems particularly well-positioned to help with situations like these since it’s ESM-first and transpiles TS without checking—this means you could satisfy our type checker by telling it --noEmit while Vite handles your emit through ESBuild.
But overall, I think we probably need to start by solving the most common individual scenarios first before we figure out how they can be endlessly combined.
There are some combinations of targets that seem to have no possible overlap. For example, if you want to write a relative import path to a TS file in Deno, you need to include the
.tsextension. But if you want to compile that same code to work in the browser, you might be stuck, at least if you usetscalone, as our current thinking is that if we let you write.tsfile extensions we would force you to compile with--noEmit.
Well there's three (possible) solutions here actually:
- The commonly asked for extension rewriting, this is fairly trivial for static imports but is quite a bit more tricky for dynamic imports
- Require a different
outDirand emit as.tsexcept without types, this is kind've strange but browsers do not care about extensions so as long as the server served the files withtext/javascriptcontent type they will just work - Emit as
.jsbut preserve.tsspecifiers, while browsers won't quite work out of the box, once support for import maps is wider it'll be possible to map.jsfiles to their.tscounterparts
@cawoodm do you have the Deno extension for VS Code enabled? https://deno.land/[email protected]/vscode_deno My understanding is that the IDE experience with Deno is pretty well solved...
I did indeed but there were teething issues which have since disappeared. import { Foo } from "./types.ts"; is now accepted.
@andrewbranch we (@denoland) are obviously interested in trying to solve this problem. One of the things that we have started to work on (@dsherret taking the lead) is a stand-alone emitter that would export Deno explicit extensions with something that would be extensionless or have JavaScript extensions as well as solve other challenges of consuming code written for Deno in other runtimes like Node.js (like supporting remote modules).
I think this isn't only just a Deno issue, as recent changes in TypeScript 4.5, module resolution and specifier re-writting is a complicated subject, which causes all sorts of problems, as bundlers expect one thing and runtimes support a different set.
My personal opinion is that it might be time to consider a pluggable resolver and loader for tsc? Something where a resolver plugin would be able to resolve a specifier, load the content, and provide a "emit" specifier so tsc doesn't have to get into any fights about what solution works?
I think this isn't only just a Deno issue, as recent changes in TypeScript 4.5, module resolution and specifier re-writting is a complicated subject, which causes all sorts of problems, as bundlers expect one thing and runtimes support a different set.
This is something @andrewbranch and I have discussed - the space is messy, but there is some overlap between Deno and bundlers in this regard.
My personal opinion is that it might be time to consider a pluggable resolver and loader for
tsc? Something where a resolver plugin would be able to resolve a specifier, load the content, and provide a "emit" specifier sotscdoesn't have to get into any fights about what solution works?
I don't want to get too off track, but I think the problem with a pluggable resolver is that the resolver doesn't give everything else for free. For example, you don't get the parts of the language service that know what the best path is to auto-import from, and whether that import should have a .ts extension or a .js extension, etc. - and those are problems beyond plugins that may or may not be doing the most optimal thing (e.g. calling the right APIs, caching in the right places, etc.)
Just wanted to chime as guided from my comment in the other thread and alluded to with Vite here and Snowpack, lots of tools besides Deno want to provide first class support for TS, it certainly stands as a testament to the great work of the team! But all of this is being made possible via ESM, but the ESM spec requires an extension. So in an ESM based world, omitting the extension is definitely going against the grain, and I suspect it will be so more and more going forward.
So in the interim, the current choices seem to be:
- no extension
- or use .js
Which leaves those wanting to provide first class support for TS between a rock and a hard place.
On another, note, perhaps this is something that could be reconciled through import assertions? Now that JSON and CSS are loadable via ESM, perhaps this concept could be leveraged and applied here? 🤔
import { Foo } from "./foo.js" assert { type: "ts" };
Something that gives a bit of hinting for both language and tooling authors alike?
but we’re still in the phase where we’re trying to develop a thorough understanding of all the different reasons people want this
There's two reasons why I want this:
- Type checking
.jsfiles that run in the browser without any build step. All source files are in pure.jsand all types are provided with JSDoc. With"checkJs": trueand"strict": truetypes can be used for completions and for continuous integration this way. - Type checking js/ts files that run in Deno. Ideally the Deno extension would solve this, but using the extension comes with a few other issues. Mainly https://github.com/denoland/deno/issues/13189 , https://github.com/denoland/deno/issues/15111 and https://github.com/denoland/vscode_deno/issues/595 but a few other ones as well.
I'm finding this issue to be the single biggest obstacle to writing a TypeScript library that works in all environments (Web, Node, and Deno). I'm compiling the library to a single file with microbundle. Ideally I could run the same test suite across both Node and Deno...
ts-node tape tests/*.test.ts
deno test --import-map tests/deno-import-map.json --no-check tests/
... but the supported import statements for the two environments are mutually exclusive. 😕
@jespertheend can you elaborate on the first of your two bullet points? I don’t understand how that one is related to writing .ts in import statements. If all your files are .js to begin with, everything should work with .js in import statements, no?
Ah sorry, for a second I thought this issue was about url imports as well as the .ts extension.
When I wrote that I was trying to import {assertEquals} from "https://deno.land/[email protected]/testing/asserts.ts";, which would still suffer from Cannot find module. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option? ts(2792) even if .ts imports are allowed.
@andrewbranch my observation about typescript at all and its missconception is that it does not simply relay on type inference + JSDOC expanded support then every type would get shipped via the original js or not and no additional resolve needed i never use the .ts extension and many senior devs do agree with me on that.
We are the most happy typescript users no issues that are resolve related or missing types they are all in the JS files
alllowJs + checkJs
is the holly graal
we even do create debug builds where we replace the types with real assert calls to get runtime typechecking.
If we simply add a flag that allows module resolution to work with .ts files, it will 100% contribute to the misconception that TypeScript does, can, or should emit JS files with the transformation import "./foo.ts" → import "./foo.js". We already have angry mobs demanding that this happen for import "./foo" → import "./foo.js" so if we allowed .ts extensions on there without it being very clear what use cases it is intended to serve, it would just pour fuel on that fire.
I would like to state that the argument for ./foo → ./foo.js is not nearly as strong as for ./foo.ts → ./foo.js. The .ts/.d.ts file extension very explicitly adds which file that is indented to import. In contrast to ./foo which could mean ./foo.ts, ./foo/index.ts, ./foo/index.d.ts. ./foo is a lot more ambiguous, while ./foo.ts is a lot more explicit.
I'm all for removing the amount of ambiguity so I'll gladly help the typescript compiler point to which file I'm actually referring to and if that in turn will make the typescript compiler turn those file extensions into .js to help with ESM compatibility, then it's terrific.
Explicit is almost always better than implicit, I wouldn't lose sleep over people who still want ./foo -> ./foo.js if *.ts was resolved to .js while files without file extensions wouldn't.
@andrewbranch wrote:
If we simply add a flag that allows module resolution to work with
.tsfiles, it will 100% contribute to the misconception that TypeScript does, can, or should emit JS files with the transformationimport "./foo.ts"→import "./foo.js". We already have angry mobs demanding that this happen forimport "./foo"→import "./foo.js"so if we allowed.tsextensions on there without it being very clear what use cases it is intended to serve, it would just pour fuel on that fire.
FWIW, I was not aware of this issue when I filed https://github.com/microsoft/TypeScript/issues/49083, but ~~it appears I’m now part of the angry mob you refer to~~ Sorry, I misread: I’m not asking for a import "./foo" → import "./foo.js" transformation, but I will happily join an angry mob asking for the import "./foo.ts" → import "./foo.js" one :)
I appreciate you seem to be willing to actually work on this, but unfortunately it seems we’re not getting close to a solution. Meanwhile the node16 module resolution is about to be released which is also pouring fuel on that same fire in its own way.
As @Lilja states, support doesn’t need to extend beyond explicit, full paths. But it would really be great if we could get some support for this.
As for @andrewbranch’s other point:
Such a flag would look like a gift to Deno users, but it would actually be woefully incomplete for them. There are obviously a lot of other parts of module resolution that are either specific to Deno or shared between Deno and the browser that we don’t currently support, and I think those users would be confused and disappointed that we stopped so far short of proper support for these things.
This seems to be a classic defeatist argument. Just because you cannot solve all problems should not be an argument to not solve any.
First, let me start with a (hopefully) constructive suggestion: let's please remove the "support extensionless imports" from the main scope and stop discussing it alongside the "add support for importing .ts extension". In my opinion it adds unnecessary controversy to any extension-related problem — and it is more or less clear that probing for different extensions is not feasible for numerous of platforms like Web and Deno (but not limited to those two). So I suggest we stop mixing these two problems and assume that we do not want to write extensionless imports and have TSC magically guess what we meant.
Now, on "import .ts" thing: since I've read a lot of different interpretations of "how relative imports in TypeScript should work", I'd like to draw a quick summary before offering my own perspective. As of May 2022 things seems to look like this:
- The official stance on the subject seems to be "we're 100% committed to not rewriting JavaScript code", effectively prescribing typing
import './foo.js'whilst referring to'./foo.ts'sitting right next to the file you have open — simply because this is how your files will look like when they will be compiled bytsc. This conclusion seems to ignore a lot of argumentation stating the behaviour of browsers, Deno, custom Node module resolvers and simply the fact that it doesn't make any sense for relative import to refer to a non-existent resource (I will elaborate on this point a bit later). It also remains a closed conversation, so there is no way for other people to understand whether this stance has changed. - The issue is raised a lot, and keeps being raised and reopened because clearly "something isn't quite right" (i.e. various tools and platforms just don't get along).
- The issue is also a biggie: it comes up every now and then as a single big blocker towards a greater goal of achieving a more uniform module loading across platforms.
- Interestingly, things are much easier with simple JavaScript modules, because, well, there's only one way of importing a
.jsfile that doesn't need to be compiled/transpiled (remember, extensionless imports are out of scope!) — this obviously does not play in TypeScript favour. - People upset with lack of decisiveness around this seemingly simple issue start to question the general direction of "where all of this is going", so much so that a lot of folks are opting out of TS syntax to stick with JavaScript + TSDoc and/or
.d.ts(which in my opinion is a considerable step down). - Circling back to tools:
- Webpack/ts-loader wants
.tsimports and won't resolve non-existent.jsunlessresolve-typescript-pluginis used - Same for Deno: if you import a relative
.js, it expects the actual file to exist relatively to the file you're currently editing (which makes total sense) - Same is true for virtually any other platform that wants to avoid probing for extensions (e.g. Node + custom resolver that tries to not do a lot of potentially expensive calls to a remote registry).
- TSC and VSCode will both complain on relative
.tsimports saying "An import path cannot end with a '.ts' extension".
- Webpack/ts-loader wants
Now as a human the way I have always imagined relative paths work is: you see something like ./foo/bar you think "ok, where am I right now?" — depending on the answer to that question you'll know how to resolve that relative path to an absolute one.
Specifically:
- if I'm in my editor, I have a file open. If it's a TS file at
src/main/foo.tsand it saysimport './bar.js', then my instinct would be too look forsrc/main/bar.js— but never atout/main/bar.js. - if I'm looking at a script loaded from
https://example.com/coo/paa/foo.jsand it saysimport '../bar.js'inside, then my instinct is to look athttps://example.com/coo/bar.js; if it saysimport './baz.ts', then I'm looking athttps://example.com/coo/paa/baz.ts. - The two examples above are pretty in-line with how Browsers and Deno work.
Finally, in my opinion, saying import './foo.js' doesn't make too much sense not only because the actual file doesn't exist relatively to the source I'm editing, but also because it says .js — but implies also importing .d.ts for the type checking to work.
So given all the above & beyond, I think it's no longer fair nor feasible to simply shrug off the issue with a "wontfix because of a design goal". Big issues like these may warrant a change in the design goals if it's for a greater good.
@inca your observations are all correct but i tracked that also down even more deep.
- A function should only do 1 thing and the resolve stuff always does many splitting the commands into more meaning full once would clear the dust once and for all and would even allow to adopt more fast in other engines
- importJson, importScript, import /* the ecmascript module one with fetch */,
- taking the same syntax and produce diffrent results is always headache.
- using the .js extension + JSDOC Annotations or with generated .d.ts is the only useable way avoiding .ts extension as it leads to mixup headache is always speeding up coding.
- avoid mixed aliases that return dynamic results and replace them with clear single meaning entrypoints that shim the environment.
- avoid environment detection and find other ways to make clear your running in the correct environment let environment detection never be part of your main code.
Rule of Thumb
.ts files are never part of the code they are additional tooling files that can lead to declarations and or code but the code is optional. If you make .ts files part of your code your entering dark worlds where cat's can die.
conclusion
as .js + generated or handwritten .d.ts is the only usabele formart to author and ship without quirks the whole issue should be closed wont-fix is the right strategie as .ts needs to die anyway.