Typescript: Consider allowing access to UMD globals from modules

Created on 5 Aug 2016  ·  73Comments  ·  Source: microsoft/TypeScript

Feedback from #7125 is that some people actually do mix and match global and imported UMD libraries, which we didn't consider to be a likely scenario when implementing the feature.

Three plausible options:

  1. Do what we do today - bad because there's no good workaround
  2. Allow some syntax or configuration to say "this UMD global is actually available globally" - somewhat complex, but doable
  3. Allow access to all UMD globals regardless of context - misses errors where people forget to import the UMD global in a file. These are presumably somewhat rare, but it would be dumb to miss them

Sounds like it would work but probably wouldn't:

  1. Flag imported UMD modules as "not available for global" - bad because UMD modules will be imported in declaration files during module augmentation. It'd be weird to have differing behavior of imports from implementation files vs declaration files.

I'm inclined toward option 3 for simplicity's sake, but could see option 2 if there's some reasonably good syntax or configuration we could use in a logical place. Detecting the use of a UMD global in a TSLint rule would be straightforward if someone wanted to do this.

One path forward would be to implement option 3 and if it turns out people make the "forgot to import" error often, add a tsconfig option globals: [] that explicitly specifies which UMD globals are allowed.

Add a Flag Committed Suggestion good first issue help wanted

Most helpful comment

+1 on this. I was just trying to use React with SystemJS, and as React doesn't bundle very well, I'm just loading it straight from the CDN in a script tag, and thus the React/ReactDOM objects are available globally.

I'm writing code as modules as best practice, but this will be bundled (Rollup) into one runtime script that executes on load. It's a pain (and a lie) to have to import from react/react-dom, and then configure the loader to say "not really, these are globals" (similar to the example WebPack configuration given in https://www.typescriptlang.org/docs/handbook/react-&-webpack.html ). It would be much easier (and more accurate) to simply have these available as globals in my modules. The steps I tried, as they seemed intuitive, were:

  1. npm install --save-dev @types/react @types/react-dom
  2. In my tsconfig.json: "jsx": "react", "types": ["react", "react-dom"]
  3. In my module: export function MyComponent() { return <div>{"Hello, world"}</div>; }
  4. Similarly: ReactDOM.render(...)

However this results in the error React refers to a UMD global, but the current file is a module. Consider adding an import instead.

If this just worked, this would be far simpler than pretending in the code it's a module, then configuring the loader/bundler that it's not. (Or alternatively, I kinda got it to do what I expected by adding a file containing the below. Now my modules can use React & ReactDOM as globals without error, but it's kinda ugly/hacky - though there may be a simpler way I've missed):

import * as ReactObj from "react";
import * as ReactDOMObj from "react-dom";

declare global {
    var React: typeof ReactObj;
    var ReactDOM: typeof ReactDOMObj;
}

All 73 comments

Instead of allowing access to all UMD globals regardless of context, wouldn't it be simpler to only allow access to the UMD global if the UMD module has been explicitly "referenced" (not imported) via either the ///<reference types=<>> syntax, or via the types configuration in tsconfig.json ?

In other words, why not just allow ///<reference types=<>> to be used within a module?

If we said /// <reference type="x" /> meant that x was globally available _everywhere_, it's frequently going to be the case that some .d.ts file somewhere is going to incorrectly reference things that aren't really global (I can tell you this because I've been maintaning the 2.0 branch of DefinitelyTyped and it's an extremely common error).

Conversely if it's only available _in that file_, then you're going to have to be copying and pasting reference directives all over the place, which is really annoying. These directives are normally idempotent so introducing file-specific behavior is weird.

I see. Well, if this isn't affecting anyone else, perhaps it's better to maintain the current behavior. I'll just have to transition fully towards importing things as modules.

Edit: glad to see this does affect many people besides myself.

Current theory is to just ask people to migrate to using modules or not. If other people run into this, please leave a comment with exactly which libraries you were using so we can investigate more.

I am using lodash, which does not come with typings of its own. I also have a situation where in my runtime environment it's easiest to use relative path import statements. So, I have a combination of import statements with local relative paths and folder relatives ('./foo' as well as 'N/bar').

If I manually copy the @types/lodash/index.d.ts to node_modules/lodash/ I can get things to typecheck.

Up till now my workaround was using ///<amd-dependency path='../lodash' name="_"> (and no import statement). With this combo, the @types/lodash definitions would be seen 'globally' by the compiler and still have the correct relative path ( ../lodash ) in the emitted JS.

I hope this is a scenario close enough to this issue?

Can you clarify

I also have a situation where in my runtime environment it's easiest to use relative path import statements.

and

If I manually copy the @types/lodash/index.d.ts to node_modules/lodash/ I can get things to typecheck.

please? I'm not familiar with this scenario, so what is the purpose of this and why is it helpful?

Hi guys,

I'm in the process of looking for a solution to current @types/bluebird declaration problem (please don't spend time reading it). I found, that the problem could be solved, by adding export as namespace Promise; to the .d.ts, but then I run into the problem described by this github issue.

In short, I'd like the following to work:

  1. git clone -b vanilla-es5-umd-restriction-problem https://github.com/d-ph/typescript-bluebird-as-global-promise.git
  2. cd typescript-bluebird-as-global-promise
  3. npm install
  4. Edit node_modules/@types/bluebird/index.d.ts by adding export as namespace Promise; above the export = Bluebird; line.
  5. npm run tsc

Current result:
A couple of 'Promise' refers to a UMD global, but the current file is a module. Consider adding an import instead. errors.

Expected result:
Compilation succeeds.

This problem is particularly difficult, because it's triggered by Promise usage in both dev's code and 3rd party code (RxJS in that case). The latter assumes that Promise is global (provided by JS standard), therefore will never change to using e.g. import Promise from std; // (not that "std" is a thing).

I'd really appreciate a way to use UMD modules as both importable modules and as globals.

Thanks.

----------------------------- Update

I ended up solving this issue differently (namely: by Promise and PromiseConstructor interfaces augmentation).

The "globals": [] tsconfig option seems preferable to making them visible everywhere. With UMD declaration becoming the norm, the odds of accidentally forgetting to import a module are high. Please consider retaining the current behavior. This should be an error.

Anecdotally I remember when moment removed their global window.moment variable in a point release. We thought we had been judiciously importing it everywhere, but we had forgotten about 5 places.

Of course a UMD package will be available in the global scope at runtime, but when it becomes available depends on the order in which other modules are loaded.

+1 on this. I was just trying to use React with SystemJS, and as React doesn't bundle very well, I'm just loading it straight from the CDN in a script tag, and thus the React/ReactDOM objects are available globally.

I'm writing code as modules as best practice, but this will be bundled (Rollup) into one runtime script that executes on load. It's a pain (and a lie) to have to import from react/react-dom, and then configure the loader to say "not really, these are globals" (similar to the example WebPack configuration given in https://www.typescriptlang.org/docs/handbook/react-&-webpack.html ). It would be much easier (and more accurate) to simply have these available as globals in my modules. The steps I tried, as they seemed intuitive, were:

  1. npm install --save-dev @types/react @types/react-dom
  2. In my tsconfig.json: "jsx": "react", "types": ["react", "react-dom"]
  3. In my module: export function MyComponent() { return <div>{"Hello, world"}</div>; }
  4. Similarly: ReactDOM.render(...)

However this results in the error React refers to a UMD global, but the current file is a module. Consider adding an import instead.

If this just worked, this would be far simpler than pretending in the code it's a module, then configuring the loader/bundler that it's not. (Or alternatively, I kinda got it to do what I expected by adding a file containing the below. Now my modules can use React & ReactDOM as globals without error, but it's kinda ugly/hacky - though there may be a simpler way I've missed):

import * as ReactObj from "react";
import * as ReactDOMObj from "react-dom";

declare global {
    var React: typeof ReactObj;
    var ReactDOM: typeof ReactDOMObj;
}

I also agree with options three plus globals: [] backup. That seems pretty intuitive to new and old users and would give the exact functionality that people need.

I'm not a specialist on the code so I can't really say if 2 would be more preferable or not but I think it would also be intuitive given the configuration for it is straightforward.

If I wanted to look into help implementing any of these where should I go?

This really should be behind a flag. It is a massive refactoring hazard. Even if the flag is specified true by default. I think this has to continue to work in the original scenario otherwise we're losing the primary benefit of UMD declarations.

React doesn't bundle very well

@billti can you elaborate?

It's a pain (and a lie) to have to import from react/react-dom, and then configure the loader to say "not really, these are globals"

The only reason I wrote that in the tutorial is because using externals cuts down on bundle-time. If you use the global React variable without importing, you can't easily switch to modules later on, whereas imports give you the flexibility of using either given your loader.

See this issue (https://github.com/rollup/rollup/issues/855) for one example on how they're trying to optimize bundling and the sizes observed. Effectively in my setup (using Rollup) I saw minimal size gains bundling React, so I'd rather just serve it from a CDN. To me that has the benefits of:

a) Less requests (and bandwidth) to my site.
b) Less time taken to bundle in my build chain.
c) Less code to re-download on the client every time I push a change (as only my code is in the bundle that gets re-downloaded, and React is still in the client cache unmodified - thus getting 304s).

Looking in the Chrome Dev Tools on loading the site, React and React-dom (the minified versions), on a GZipped HTTP connection, are only 47kb of network traffic, which is less than most images on a site, so I'm not worried about trying to reduce that much anyway, unless there's really big gains to be had (e.g. 50% reduction).

As an addendum: I'd also note that without this option, you are also forcing folks to use a bundler that elides these imports, as the TypeScript compiler itself has no configuration for saying "this module is really a global", and will thus emit imports (or requires, or defines) for modules which wouldn't resolve at runtime.

@billti SystemJS has full support for this scenario. You can swap between using a locally installed package during development and using a CDN in production. It also has full support for metadata which specifies that all imports should actually indicate references to a global variable that will be fetched once and attached to the window object when first needed, in production this can come from a CDN. I haven't done this with react but I have done it with angular 1.x

Thanks @aluanhaddad . Interesting... I was actually trying to get something similar working that led me to this roadblock, and couldn't figure it out, so just this morning asked the question on the SystemJS repo. If you can answer how to achieve https://github.com/systemjs/systemjs/issues/1510 , that'd be really helpful :-)

Note: My other comment still stands, that the emit by TypeScript itself is not usable without this, as you need something like SystemJS/WebPack/Rollup etc... to map the imports to globals for the code to run.

I'll take a look and see if I can make a working example, I haven't done it in quite a while and I don't have access to the source code that I had at the time but I'm hundred percent sure it's possible.

To your second point, that's exactly what SystemJS does. It will map those imports to the global and understands that the global is actually being requested and has already been loaded. The output is definitely usable

FYI: I got this working in SystemJS using the SystemJS API and added my solution on https://github.com/systemjs/systemjs/issues/1510 . Thanks.

Re my second point: Yes, I know that's exactly what the loaders can do. That's my point, they can map an imported module to a global, but TypeScript can't - so you have to use a loader to make your code valid at runtime. So it's a catch-22 with this original issue, where you can't declare that the global (in this case React) is available in the module, you have to import it as if it were a module (which it isn't).

My other comment still stands, that the emit by TypeScript itself is not usable without this, as you need something like SystemJS/WebPack/Rollup etc... to map the imports to globals for the code to run.

@billti I don't understand. What is a scenario in which your only option is to use the global version of a module, but TypeScript doesn't allow you to do that? I've only seen scenarios where a library is available as both a global and a module.

@DanielRosenwasser I think he means that React is actually a global at runtime as in a member of the global object, because of how it is being loaded.

@billti Awesome that you got it working.

Re your second point: I see what you mean.

I suppose that my feeling is that, in a browser scenario, because you need to either use a loader like RequireJS or a packager like Webpack because no browser supports modules yet it doesn't make any difference. (I hear Chakra has it available behind a flag). So there is no way to run the code at all without an additional tool. It is sort of an implication of the output containing define, require, or System.register that the emitted _JavaScript_ code is not likely to be portable. However, I do see the importance of the "module vs not a module" distinction.

You can use this workaround to at least only refer to the "module" once.

_shims.d.ts_

import __React from 'react';

declare global {
  const React: typeof __React;
}

Then you can use it anywhere else without importing it.
Also this is nicely explicit, if a bit kludgy, because you are saying that React has become global and that is also the reason you do not have to import it anymore.

Re your shims.d.ts, if you go up a few posts, you'll see that's what I did for now (great minds think alike) ;-)

I can get it working one of several ways now, that's not the point. We're trying to make TypeScript easy to adopt, and have users falling into the pit of success, not the pit of despair. With that in mind, I often ask myself two question when trying to use TypeScript and hitting issues: a) Is this valid code, and b) Are customers going to try and do this.

Seeing as I had the (non-TypeScript) version doing what I wanted in Babel in roughly the time it took me to type it in, I think it's fair to say the code is valid. As the installation page of the React docs shows how to use script tags from a CDN to include React, I'm guessing a number of folks will try that too. (FWIW: I've spent more time than I care to remember working with various JS modules and loaders, so it's not like I'm unaware of them, I just wanted to write my code this way).

If TypeScript is not going to support certain valid patterns of writing code, we should try to make that immediately obvious and steer folks right (which is a challenge to do in error messages or concise docs). But personally, I don't think TypeScript should be not supporting patterns because we don't think they're "best practices" or "canonical". If the code is valid, and some JavaScript devs may want to write it, then TypeScript should try to support it. The more we require that they change their code and reconfigure their build pipeline to get TypeScript working (like is recommended here to migrate my trivial app), then the less devs will move over.

As to the solution... just spit-balling here, but perhaps the "lib" compiler option, which already effectively defines what APIs are available throughout the project, could also take @types/name format values for libraries to add globally (and even support relative paths maybe).

We're trying to make TypeScript easy to adopt, and have users falling into the pit of success, not the pit of despair.

I think that we are trying to lead users to the pit of success right now. If a module only conditionally defines a global, then you are accidentally guiding users into using something that doesn't exist. So I see a few different options:

  1. Create an augmented export as namespace foo construct that is only visible if not imported by a module.
  2. Do nothing, and keep pushing people to use the import - this is more or less fine in my opinion, since we've made the error message reasonably prescriptive anyway.
  3. Allow people to use the UMD from everywhere - I'm honestly not as big on this idea.

@billti

Re your shims.d.ts, if you go up a few posts, you'll see that's what I did for now (great minds think alike) ;-)

Sorry, I missed that, very nice ;)

I don't think TypeScript should be not supporting patterns because we don't think they're "best practices" or "canonical"

I don't think TypeScript is being proscriptive here, I think it is doing what it claims, telling me that I have an error. A lot of libraries have demos and tutorials where they load themselves' as globals and then proceed to use ES Module syntax. I don't think they are being the greatest citizens by doing this, but that is another discussion.

That said, if modules are primarily used as a _perceived_ syntactic sugar over globals, then their failure is at hand because they are not a syntactic sugar at all. If anything they are a syntactic salt (perhaps a tax?) that we consume for benefits like true code isolation, freedom from script tag ording, explicit dependency declaration, escape from global namespace hell, and other benefits. The syntax for modules is not ergonomic, it is verbose at best, but it is the semantics of modules that make it worthwhile.

I think if people use TypeScript, in .ts files at least, I assume they want to get the benefits of strong static code analysis. Babel doesn't do this, assuming React exists but having no knowledge of it. This is true even though ES Modules have been deliberately specified to be amenable to static analysis.

@DanielRosenwasser

Create an augmented export as namespace foo construct that is only visible if not imported by a module.

That sounds like the best way to resolve this.

Here's another case where this caused problems:

In a project I'm currently working on, we mix local includes (mostly for historic reasons) with npm modules. In the end everything is joined using Rollup or Browserify, so that's fine.

I use a .js file from the emojione project that I simply copied into the codebase. Later I added the type declarations for it to DefinitelyTyped: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/13293 I thought I could now simply load the types and everything would work. But that doesn't seem to be the case, because TypeScript won't let me access the global.

The reason why I'm not moving to the npm module is that the npm module also bundles multiple megabytes of sprites and PNGs. I just need that one 200KiB script. With type declarations.

With AngularJS, the workaround was declare var angular: ng.IAngularStatic. But that doesn't work with namespaces, right?

@dbrgn You are experiencing a different issue. If the module is actually a global, then your type definition is incorrect. It neither declares a global, nor is a UMD style declaration (this is about UMD style declarations), it actually declares a pure ES Module only.

If the module represents a global, don't export at the top level of the file, that makes it a module.

With AngularJS, the workaround was declare var angular: ng.IAngularStatic. But that doesn't work with namespaces, right?

It works with namespaces.

The outcome of the discussion at our design meeting was that we are considering always allowing the UMD, and adding a flag that enforces the current restriction. The restriction will also be extended to work on accessing types from a UMD global.

Having thought about this more, I still think the better thing to do is create a new declaration kind. This flag is less discoverable than the new syntax, which only needs to be written once by the declaration file author.

This is desperately needed for existing code and tools. Until such time Javascript stops playing fast and lose with module systems, we need flexibility to work with code as it exists. Emit a warning, but don't fail the build. I've wasted days on dealing with making legacy code play nice with rollup and typescript.

UGH.

I know there are a lot of folks that like to laugh at Java, but basic java modules at least work. Jars work

I don't have to fit 14 different ad hoc module standards, or try and compile a js module from source files in the format that the rollup/bundling tool of the day will actually consume without pooping itself, and also will generate a module format that will play nice with Typescript import/export statements and third party d.ts files so that TSC will actually decide to build the code instead of whining about something where you're going "JUST USE THE DARN IMPORT, IT WILL BE A GLOBAL AT RUN TIME".

The shims.d.ts hack works well. But ugh.

Temporary solution for those using Webpack https://github.com/Microsoft/TypeScript/issues/11108#issuecomment-285356313

Add externals to webpack.config.js with the desired UMD globals.

    externals: {
        'angular': 'angular',
        'jquery': 'jquery'
        "react": "React",
        "react-dom": "ReactDOM"
    }

I think this should be possible to make it easier to migrate existing codebases.

I have a project implemented with requirejs where jQuery is included as global, becasue there are some plugins that extend jQuery only if it's found as a global.

The code in some of the modules depends on that plugins, which wouldn't be available if jQuery was imported as a module. To make this work I would have to modify all the plugins to work with jQuery loaded as a module, loading them also as modules (an guessing where they are necessary).

Besides, there are also pages which use javascript without module loaders. So, the plugins should work both with globals and modules.

Apart from jQuery, there are other scripts with the same problem like knockout and others. This makes migrating the project a nitghtmare. Or, from a realistic point of view, infeasible.

Of course, this isn't the best pattern, and I wouldn't use it in a new project. But I don't think I'm the only one with this problem

Would it make sense to use types in tsconfig.json for this? E.g. without types set, you get the current implicit behaviour and with types set, you're literally saying "these things are globals" and can force the UMD namespace to appear globally. That's kind of the behaviour that exists today anyway (minus the force global). This is opposed to introducing a new globals option.

I think that's a good idea. In my case there are scripts which use an UMD library as global, and others as module. I could solve this problem with two different tsconfig.json which address each case. Really straigthforward.

@blakeembrey Although using types makes sense, I'm not too keen on the notion of overloading it since it already has problems. For example, the <reference types="package" /> construct already has the limitation that it does not support "paths". The "package" must refere to a folder name in @types

I'm having a tough time following this conversation. Have there been any updates or planned resolutions for this? It seems like this is something that could be useful in scenarios such as when lodash is such an integral part to an application, or when more 3rd party libraries convert to a more modularized structure instead of just relying on being on the window.

Is there a planned way to address this or at least to document how this should be solved with the current available release?

Hi @mochawich I am getting the following error with using defining React as externals and not use the declare global syntax:

TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.

@cantux TypeScript does not read Webpack configuration. If you have made React available globally, you can declare that, but why not use modules?

@aluanhaddad mostly because I was confused with the work done by import call. I fiddled some, is the following statements correct?

We are paying the small fee of making a request when we import a module. This makes sure that what we are using is available in the memory, if it was previously requested, module is loaded from cache, if module doesn't exist, it is fetched. If you like to circumvent this request, you just define something as global and Typescript blindly trusts you that it is available(and if you use a smart bundler import statements can even be replaced/removed).

If these are correct, we could remove the comments for brevity, thread is a giant as is.

As @codymullins asked above, can someone summarise the current workaround for this problem? I just updated lodash type definition and got plenty of TS2686 errors.

My current workaround was to hack the typedef file to conform to the old, working standard but that's not viable if more typedef files start to break.

My scenario is as follows:

  • in my single-page apps I import a number of libraries (including lodash) in a