Skip to content

TypeScript and <script type="module"></script> #13422

Description

A first implementation of <script type="module"><script> just landed in Safari Technology Preview 21, so I tried to use it on a TypeScript / Angular project, to finally get rid of system.js.

First issue, resolved but unconvenient : ES6 modules paths must include the '.js' extension. Fortunately, TS 2.0+ knows how to resolve import { Something } from './test.js' into import { Something } from './test.ts' while in dev. But it's a huge change to common practices and a lot of refactoring : official TypeScript docs, Angular, RxJS and so on have all encouraged until know to use the no extension form (import { Something } from './test').

Second issue, unresolved : TS provides many options to tell the compiler how to resolve paths like import { Component } from '@angular/core'. But it never transforms the imported path in the compiled files. '@angular/core' will stay '@angular/core' in the transpiled files. After investigation, I've read in many issues it is by design.

It's OK with loaders like system.js and others, which provide similar path mapping options. But with <script type="module"><script>, as far as I know, there is no configuration, so import { Component } from '@angular/core' will always fail.

I am missing something, or does that really mean that we won't be able to use the native loader with TypeScript projects ?

If I'm right, is it possible to add an option to force path transformation in compiled files ?

Activity

  1. aluanhaddad commented on Jan 14, 2017

    @aluanhaddad
    Contributor

    Just a comment. It seems ironic that you wish to

    to finally get rid of system.js

    When it is about the only option comes that close to what you seem to be trying to achieve.

    But it's a huge change to common practices and a lot of refactoring : official TypeScript docs, Angular, RxJS and so on have all encouraged until know to use the no extension form (import { Something } from './test').

    Personally, I think extensionless imports are far better, they make your code far more portable.

    I suspect the native loader will eventually support configuration. Of course the specification is in major flux, but there is likely to be a configurable API to perform abstracted name resolution for imports like

    import {Component} from '@angular/core';

    because it is clearly needed and because the issue has been discussed at least to some extent. I've heard discussion of how to resolve

    import $ from 'jquery';

    You may find this repository interesting https://github.com/whatwg/loader/

    If I'm right, is it possible to add an option to force path transformation in compiled files ?

    No this is not possible at present.

  2. cyrilletuzi commented on Jan 14, 2017

    @cyrilletuzi
    Author

    I don't see what's ironic in wanting to depend on native and standard features, instead of librairies...

  3. aluanhaddad commented on Jan 14, 2017

    @aluanhaddad
    Contributor

    Sorry I am a dyed in the wool SystemJS user (actually I'm not sorry 😜). But the whole point of SystemJS is to get out of the way, when native loaders are available, but until then polyfill the loader specification (and provide other features such as CommonJS interop).
    In other words it has built in obsolescence, by design, so it does not interfere with the future it seeks to enable today.

    Seriously SystemJS is great. Not every framework uses it well, for example Angular 2, made poor use of it and is now removing it. Look at Aurelia as an example framework that, while not requiring SystemJS, uses it by default and plays to its strengths showing how elegant it can be.

  4. cyrilletuzi commented on Jan 14, 2017

    @cyrilletuzi
    Author

    It's not a charge against SystemJS. Yes, it is a nice tool. But as you said it very well, it was designed as a temporary tool until the native loader is finally here.

    So now the central part of it is there (<script type="module"></script>), but unusable in a real case : I can't see the point if we can just load a few local scripts, and no librairies. So I'm confused.

    Hopefully configuration is coming. Or hopefully TypeScript will manage it.

  5. aluanhaddad commented on Jan 15, 2017

    @aluanhaddad
    Contributor
  6. Avol-V commented on Jan 19, 2017

    @Avol-V

    Personally, I think extensionless imports are far better, they make your code far more portable.

    SystemJS didn't recommend to use default extension as well.

  7. mhegazy commented on Jan 26, 2017

    @mhegazy
    Contributor

    i am not sure I see why this is a TS issue. If you put the full file path in the module, it should work. Or putting it diffrentelly, how could you do the same with a plain .js file with the typescript compiler completely out of the picture?

  8. Avol-V commented on Jan 26, 2017

    @Avol-V

    Whether TypeScript transforms paths or not, it could be an option to add extension to the names of modules (or it could convert .ts to .js).

  9. mhegazy commented on Jan 26, 2017

    @mhegazy
    Contributor

    import .. from "./foo.ts" is not allowed so it is either "./foo.js" or just "./foo". I would recommend using consistent extension and always specify .js.

  10. cyrilletuzi commented on Jan 27, 2017

    @cyrilletuzi
    Author

    i am not sure I see why this is a TS issue. If you put the full file path in the module, it should work

    As far as I know, TypeScript doesn't support absolute paths. So no, it doesn't work.

    After a deeper thinking, I think the current behavior is really not normal. As a transpiler, TS should produce standard and ready-to-work JavaScript. TS also choose the very good design option to be "just" enhanced JS, ie. backward compatible with normal JS.

    import { Something } from './something' is clearly not standard JS.

  11. mhegazy commented on Jan 27, 2017

    @mhegazy
    Contributor

    As far as I know, TypeScript doesn't support absolute paths. So no, it doesn't work.

    what do you mean absolute path? can you share an example of an app with a module that is absolute? a CDN path should work with a path mapping entry.

    import { Something } from './something' is clearly not standard JS.

    We do not know what node will do yet. but if node considers this invalid filename, the compiler can flag it as an error. either ways, you should just write:

    import { Something } from './something.js'
  12. cyrilletuzi commented on Jan 27, 2017

    @cyrilletuzi
    Author

    We do not know what node will do yet. but if node considers this invalid filename

    It's already an invalid filename for browsers, which is the main use case of TypeScript.

    you should just write: import { Something } from './something.js'

    I agree, that's what I do now to be future proof. Problem is, like I said on first message, that it's a huge change to common practices and a lot of refactoring : official TypeScript docs, Angular, RxJS and so on have all encouraged until know to use the no extension form (import { Something } from './test').

    For absolute paths, I mean a code like this :

    import { Component } from '/node_modules/@angular/core/index.js';

    Otherwise, I need to do things like this :

    import { Component } from '../../../node_modules/@angular/core/index.js';

    First, it's a mess, second, it doesn't work if outDir is not at the same path level.

  13. cyrilletuzi commented on Jan 27, 2017

    @cyrilletuzi
    Author

    you should just write: import { Something } from './something.js'

    And most importantly, I can refactor my code, but I can't refactor librairies I use...

  14. mhegazy commented on Jan 27, 2017

    @mhegazy
    Contributor

    It's already an invalid filename for browsers, which is the main use case of TypeScript.

    No it is not if you are using requireJS, Browserify, Webpack, SystemJs, etc.. i.e. any thing other than a browser that supports native ES6 modules, and as i far as i know, there are not many of these around today.

  15. mhegazy commented on Jan 27, 2017

    @mhegazy
    Contributor

    For absolute paths, I mean a code like this :

    The loader spec does not really talk about how these absolute paths are allowed. When we have a clear description of that, allowing such should not be hard.

  16. 47 remaining items

  17. tjcrowder commented on May 7, 2021

    @tjcrowder

    Please consider reopening this and implementing it.

  18. antongolub commented on May 11, 2021

    @antongolub

    It seems that proper way fix will not be available soon, so I wrote a small js-script as an alternative to sed-based patcher mentioned above.
    To save someone else's time:

  19. orta commented on May 11, 2021

    @orta
    Contributor

    Given how the ecosystem has changed since 2017. To try summarize the TS position here in mid 2021:

    • TypeScript won't be changing your import specifiers for you (e.g. import {x} from "./x/y/z.ts" to import {x} from "./x/y/z.js" ) it breaks one of the founding guidelines of only erasing types (and TS would not be where it is today if it broke its own design rules) - you can write this in ESM code today and it will work. I do it.

    • An import like import { Component } from '@angular/core' only works today with node-style resolution strategies, but the import map spec for JS will solve that problem, and once it's settled and standardized TypeScript will support that too.

    Re: original point about resolving TS in modules in html files- I've been doing some work on that in microsoft/vscode#121517

  20. cdalexndr commented on Jul 14, 2021

    @cdalexndr

    Instead of waiting X years for caniuse import maps to be implemented by all browsers, typescript could implement this itself.

    For example, typescript already has something similar for lookup (paths), but it doesn't transform output js import paths:

        "paths": {
          "jquery": ["node_modules/jquery/dist/jquery"]
        }
    

    Adding an import map feature, for example:

        "importMap": {
          "jquery": ["/node_modules/jquery/dist/jquery.js"]
        }
    

    would allow to transform import $ from "jquery" to import $ from "/node_modules/jquery/dist/jquery.js".

  21. NigeNigeNige commented on Sep 15, 2021

    @NigeNigeNige
    • TypeScript won't be changing your import specifiers for you (e.g. import {x} from "./x/y/z.ts" to import {x} from "./x/y/z.js" ) it breaks one of the founding guidelines of only erasing types (and TS would not be where it is today if it broke its own design rules) - you can write this in ESM code today and it will work. I do it.

    The TypeScript compiler already rewrites import statements depending on the value of config.compilerOptions.module. The issue here is that when omitting a file extension from an import statement in a source file (as has been standard practice in front-end development for a long time and is prevalent through many third-party packages in that domain), the ES2015, ES6, ES2020 and ESNext settings cause the compiler to produce code that does not work on these target environments, despite tsc stating that there are no errors.

    The problem isn't that our TypeScript code is wrong - it passes all compile and linting steps perfectly - it's that the compiler is doing the wrong thing when converting to JavaScript for these target module systems.

  22. srcspider commented on Nov 18, 2021

    @srcspider

    With regard to this issue, not only does Typescript use non-extension imports in it's own documentation on modules https://www.typescriptlang.org/docs/handbook/module-resolution.html the VS Code editor, which is also made by microsoft needs special settings to even work https://2ality.com/2021/06/typescript-esm-nodejs.html#visual-studio-code

    I can understand sticking to a design principle. But I have to ask, WHO is this for? Who is actually benefiting from that design principle? Since it feels like, it's literally for nobody. Especially when nobody is asking to change default behavior, just a switch to append ".js" or ".mjs" when outputting (out of which there are crazier output settings already in the language). To be honest just the confusion aspect of this issue is reason enough to add a simple output


    @ anyone stumbling here...

    If you're trying to get your node program to work, just replace whenever you would write node with node --es-module-specifier-resolution=node (node v16.13.0). Think of it as the right of passage for the language; can't be mature language with out pointless mandatory flags (it's just like how you see everyone write perl -w, welcome to the club)

    If you need it as a shell header, don't forget the -S this is the correct version:
    #!/usr/bin/env -S node --es-module-specifier-resolution=node

  23. AjayChambers commented on Jan 12, 2022

    @AjayChambers

    After tediously reading 4+ years worth of comments on this thread I must say SrcSpider ended the thread on a good note for me. The thing that confuses is me, is I don't understand why no one wants to fix this issue. I don't contribute to TS, so I don't know the project all that well, but usually path resolution issues (from what I have seen) are solvable issues. At the worst case a flag could be added to change TS behavior for ES6 Module path resolution right? IDK, maybe I am out of my league, but the way it is as it stands to be extremely annoying. So annoying it makes me wonder if I am just wasting my time working to make sure I write everything in TS. The whole point of compiling to JavaScript (transpiling) is backwards compatibility, portability, and valid code (or valid ES6 JS in this case), right? Its 2022, and no one working on the project seems to even care.

  24. quantuminformation commented on Jan 12, 2022

    @quantuminformation

    @W3Dojo don't miss out part 2

    #16577

    12 hrs of reading

  25. jogibear9988 commented on Jan 14, 2022

    @jogibear9988

    I've created a pull request wich wich solve this for raltive imports, see:

    #47436

    I've added a compiler switch:

       appendModuleExtension
    
  26. one-github commented on Jan 14, 2022

    @one-github

    I've created a pull request wich wich solve this for raltive imports, see:

    #47436

    I've added a compiler switch:

       appendModuleExtension
    

    They said it wasn't possible. Along came some guy who didn't know this and just did it. 🥳

  27. mitsukuri commented on Mar 22, 2022

    @mitsukuri

    According to my trusted sources, the rationale behind this stubborn refusal to actually make tsc emit javascript imports with, well, .js extension is as follows:

    If implemented, Typescript newcomers will cease writhing in pangs, thus disrupting the steady supply of bad vibes that keep Ctulhu asleep, and it will make Him emerge from R'lyeh, scramble the letters in all occurrences of Microsoft in existence into Coots Firm and scuttle back into the Ancient City growling unspeakable curses; And the management clearly doesn't want that!

    Let us all support the stoicism of those behind this unpopular decision and applaud their efforts to redeem the world!

  28. paul-uz commented on Aug 12, 2022

    @paul-uz

    I've created a pull request wich wich solve this for raltive imports, see:

    #47436

    I've added a compiler switch:

       appendModuleExtension
    

    How exactly do I use this? I can't find anything about this option in the documentation :/

  29. jogibear9988 commented on Aug 12, 2022

    @jogibear9988

    it was not accepted

  30. pylgrym commented on Nov 14, 2023

    @pylgrym

    I can understand sticking to a design principle. But I have to ask, WHO is this for? Who is actually benefiting from that design principle?

    I can answer this for you :-). Because I am in the same boat as you, but I actually don't want this solution..
    The problem with this proposed solution, is that having it available would cause a whole new host of different problems, even larger than the problem you/we are currently having.

    The principle that typescript/tsc will preserve import paths, and never try to magically adjust them,
    is necessary for interoperability of released library code.
    Because import paths are never adjusted, you have a guarantee that the shape of the present import paths does not depend on specific build settings.
    IF typescript would adjust the paths differently based on settings, you would suddenly

    • need to know WHICH variant of settings they were based on.
    • even worse, maybe need multiple variants of the present files, a separate subset for each possible combination of settings.

    Also, one of the invariant rules of typescript is, that if you feed it a plain javascript file, that file will be re-output unmolested/without needing any adjustments. If you had this import-path adjusting possibility, it would need to be able to adjust import paths in a plain javascript file, violating the guarantee that it won't adjust plain-javascript.

    I/we are already living variants of this nightmare.
    I am currently trying to use an npm package that contains javascript files that use weird import syntax.
    Because of this, I cannot use that npm package with
    browser-ESM imports, because the foreign-import-syntax in that npm package is not legal browser-ESM.
    Neither can I use that npm package directly in nodeJS, because the foreign-import-syntax is not legal nodeJS either.
    Instead, I am forced to go on a detective-mystery-hunt, to guess-figure-out which magic combination of bundler tools the authors of that npm package are using, to allow their fancy non-standard import syntax to work.

    This is the hell these 'defenders of typescript' are fighting against/protecting you from :-) :-(.

    I will now continue on my odyssey to unravel, how their (my 'enemies'.., not the ts-people) non-standard import-syntax can be made to work when using ESBUILD :-/.

  31. trusktr commented on Nov 14, 2023

    @trusktr
    Contributor

    Random addition: one solution to this for browser native ESM is to add a service worker to your app, and append the .js extensions in the service worker. I've had good success with that (although I much prefer just adding .js in the import statements and simply moving along while keeping complexity lower).

    I also use service workers for things like importing `.css` or other types of files.

    Basically service workers let you implement a module loader.

    What you do is you intercept the URL (f.e. for a URL ending in .css) and you have the service worker make the response be JavaScript code that can actually fetch the original file and do anything with it (f.e. the returned JS fetches the .css file and instantiates a style sheet with it and appends it to the document.head, etc). And because ES Modules are async (they can even have top-level await), fetching resources as part of a module works just fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Domain: ES ModulesThe issue relates to import/export style module behaviorNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions