microsoft/TypeScript

Consider allowing access to UMD globals from modules

RyanCavanaugh opened this issue ยท 73 comments

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.

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?

d-ph commented

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 (rollup/rollup#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 systemjs/systemjs#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 systemjs/systemjs#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.

dbrgn commented

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: DefinitelyTyped/DefinitelyTyped#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 #11108 (comment)

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 <script> tag -- they're the ones used everywhere; could load them dynamically but that's inconsequential here
  • every module in the project has a /// <reference path="../../../path/to/common-typings.d.ts" /> which, in turn, references all installed 3rd party libraries (I import application modules that are used in a particular class)
  • this way I can just type _.filter(data, predicate) and not worry about it as it's part of the plumbing

What are the current options? I can't import a d.ts file, can't import the library either because it's a plain javascript library so what to do?
I feel like I'm missing a concept here but it used to be simple: plain javascript dependencies handled separately, d.ts files to describe shapes to the compiler and that's it.

A suggestion for whoever decides how to solve it: please don't make us add tens of import/reference lines to each and every class in our typescript projects. Whatever the solution please make it so that we can make a change in one place (e.g. a config file) and fix it for the entire project.

dakom commented

the shim example mentioned above does work, though vscode is highlighting it as an error (even though it still does the completion properly!)

Please please please don't give errors when accessing UMD globals in modules. The massive project I am working on is done in AngularJS and we are using Typescript for the application, but of course we need Typescript to know about the angular UMD global and Angular types from @types/angular. You'd think it'd be as easy as adding "angular" to types in tsconfig.json, but, for whatever reason, it isn't, and TSC screams at me. As much as I wish all NPM packages were pure Typescript, most of them are plain JS and will be for a very long time. I really don't understand why TSC can't just shut up when we import a d.ts saying that a UMD global is present. This situation is more than common--every Typescript project I have ever worked on uses at least one JS library that I have to bundle myself and reference using type definitions.

Is there any update on this?

My usecase: I am working on a large existing codebase which makes heavy use of CDNs. Common utilities are imported via script tags across many pages (ex. clipboardjs, lodash). I would like to reference those global variables since they are available on the page. Without using modules, it's easy enough to have typescript compile, using /// <reference type="$name" /> at the top of relevant source files. However this stops working when trying to create modules.

It appears there have been two approaches proposed in the thread:

  1. Have the /// <reference type="$name" /> import tokens into current file's namespace only.

  2. A compiler option / configuration variable in tsconfig.json (ex. "globals", "types")

I think both approaches are good. While I agree with the criticism of option 1 by @RyanCavanaugh :

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.

I believe it's much more annoying that you can't use modules together with UMD globals at all due to the current behaviour. Some workaround is better than none.

Is this issue still outstanding? And if so, what's the current workaround?

If you install the @types package, those packages that aren't imported as modules, are made available as globals.

For example, if I npm install -D @types/underscore in the root of my project, then I can write modules that don't import anything from underscore, yet the _ global is made available (see below).

types-ref

Is that what you're after?

@billti Maybe I'm misunderstanding, but your example does not work for me.

Minimal necessary to repro:

js/foo.ts:

// <reference types="js-cookie">

import { Bar } from "./bar";

const Foo = {
	set: function() {
		Cookies.set("foo", "bar");
	},
	get: function() {
		console.log(Cookies.get("foo"));
	}
};

window.onload = function() {
	console.log(Cookies);
}

js/bar.ts

const Bar = {
	x: 3
};

export { Bar };

package.json:

{
  "name": "foo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "devDependencies": {
    "@types/js-cookie": "^2.1.0",
    "typescript": "^2.7.1"
  },
  "dependencies": {
    "http-server": "^0.11.1"
  }
}

tsconfig.json

{
	"compilerOptions": {
		"module": "system"
	},
	"files": [
		"js/foo.ts"
	]
}

Error messages:

js/foo.ts(7,3): error TS2686: 'Cookies' refers to a UMD global, but the current file is a module. Consider adding an import instead.
js/foo.ts(10,15): error TS2686: 'Cookies' refers to a UMD global, but the current file is a module. Consider adding an import instead.
js/foo.ts(15,14): error TS2686: 'Cookies' refers to a UMD global, but the current file is a module. Consider adding an import instead.

The behavior you get is dependent on whether the imported module was written as a "proper" UMD module (this is the behavior with "Cookies") or a "works both ways at the same time" module (which is how lodash is written).

The inconsistency of people correctly writing a .d.ts file that describes how the object works at runtime, and the opaqueness of the experience for developers, is why I'm leaning pretty hard in the "remove UMD global restriction" direction. We could put it under --strict with a --noStrictUMD opt-out.

The other thing I ran into was dealing with Monaco's custom AMD loader. They support some subset of AMD behavior (insert enormous eyeroll) but stomp on the global 'require' so it's really hard to use proper UMD modules with it since those modules tend to see the 'require' global and then fail to load properly into the Monaco module loader. You end up putting the UMD JS libs above the script tag for the Monaco loader and then TS complains because you're accessing the globals from a module (which you have to be to import the Monaco APIs).

@RyanCavanaugh

The other thing I ran into was dealing with Monaco's custom AMD loader. They support some subset of AMD behavior (insert enormous eyeroll) but stomp on the global 'require' so it's really hard to use proper UMD modules with it since those modules tend to see the 'require' global and then fail to load properly into the Monaco module loader.

๐Ÿ˜

Any chance of them fixing that?

I've been thinking a lot about the complexity cost of modules of late. So many interdependent, partially compatible loaders, bundlers, transpilers, package managers, and frameworks amount to a truly non-trivial amount of accredit complexity. (I'm sure you need no reminder whatsoever ๐Ÿ™‰).

As developers we have accepted toolchains that are orders of magnitude more complex than we had 5-6 years ago, and the primary source of complexity has been modules.

If we give up and start loading these UMD packages as globals, what has it all been for?

And yet... people are doing that just that. This is terrible!

I mean, this Stack Overflow Answer has 61 ๐Ÿ‘s and it has been suggesting all the wrong things for 99% of packages for the last half-year. (the author kindly updated it to mention modules as an option for UMD dependencies due to some feedback provided this morning)

This can't let all this have been in vein and go back to globals!

And yet... people are doing that just that. This is terrible!

The problem is that JS modules are terribly poorly conceived and implemented, so it's a lot better and easier to go back to using globals. If modules had been properly designed and implemented from the beginning...

We mix-and-match modules and UMD globals because it's way too much hassle to load our dependencies as modules with varying levels of compatibility with various loaders and some loaders not supporting direct dependency bundling and instead you have to use our special preprocessor that takes a minute to run.

This "feature" just means that we don't use the official UMD module support even though we actually do use UMD modules. We just export as a global from the .d.ts file and then manually have our own module of that name that re-exports everything.

Any update on this? I would really like option 2 to work:

Allow some syntax or configuration to say "this UMD global is actually available globally"

rtm commented

I'm using three.js in an Angular project. I import it as

import * as THREE from "three";

import {Vector3} from "three"; also works as expected.

With three and @types/three npm packages installed, everything works fine. Under the hood, I guess this is using three/build/three.module.js. The @types/three/index.d.ts file uses the notation export as namespace THREE, which I'm not entirely comfortable with, but hey it works.

In this particular case, the problem is that there is another related file in the three.js system called OrbitControls.js (which allows you to rotate 3d images with your finger or mouse, basically). The problem is that although this function is semi-officially part of the three.js distribution, it is a plain-old JS file found in the examples tree and directly places itself on the THREE property of the window, and directly uses other APIs it expects to find present on window.THREE. So even if I "require" the file with

require("three/examples/js/controls/OrbitControls.js");

it can't find window.THREE to put itself on, or to access other parts of the THREE system it uses. I can include the entire library directly using the Angular scripts property in angular.json (roughly equivalent to an old-fashioned <script> tag), but then if I'm not mistaken I will have loaded the library twice.

To avoid that, I removed the import * as THREE from "three"; statement, and hey, it can still resolve types such as foo: THREE.Vector3, but chokes on references like new THREE.Vector3() with the infamous

'THREE' refers to a UMD global, but the current file is a module. Consider adding an import instead. [2686]

At this point I'm thinking I'm just going to have to grab the OrbitControls.js file and ES6-ify and/or TS-ify it, which is what it seems more than one other person has already done in the form of things like `orbit-controls-es6, so maybe I should just simplify my life and use that, even though I hate putting my life in other people's hands that way.

On a semi-unrelated note, one oddity is that @types/three defines types for OrbitControls, even through the code itself is not in the three module itself. However, I can't figure out how to associate all those types defining a class called OrbitControls with anything--I would like to declare the default export of the above-mentioned orbit-controls-es6 to be of this type, but how to do so eludes me.

The solution I finally came up with, which I am deeply ashamed of, is:

import * as THREE from "three";
Object.defineProperty(window, "THREE", {get() { return THREE; }});
require("three/examples/js/controls/OrbitControls.js");

It works, although I'm slightly confused why. The required file has a line such as

THREE.OrbitControls = funtion() { };

which looks like it would end up assigning to the THREE "namespace" resulting from the import * as THREE from "three"; statement, which shouldn't work, should it?

@RyanCavanaugh asked me to copy my feedback from #26223 here:

I'm maintaining a rather large TypeScript code base (Google's internal monorepo) that has several definitely typed libraries that people depend on. Originally users would just rely on global types for libraries such as angular, before the .d.ts were turned into external modules. We then migrated the code base to use modules and explicit imports. We actually expected that export as namespaced UMD globals would always require an explicit import to use symbols, for both type and value references, and didn't even notice back when we migrated (whoops).

Allowing non-imported use of code is generally problematic for us:

  • it means code relies on global "background" type definitions, making code harder to read (in particular in repo browsers or code review w/o go to symbol).

  • it obscures code dependencies

  • it circumvents the "must have explicit build level dependency on all imports" constraints we implement in bazel, aka "strict deps".

    In a large code base, your code must have explicit dependencies, otherwise the repo becomes unmanageable. Now if you have a dependency chaing A -> B -> C and global types, it's easy to have code A compile just because B has a dependency on C. If B later removes its dependency, it breaks A, which means changes have unexpected ripple effect on the repository, violating code isolation.

  • it causes code to inconsistently import the module with one prefix for values and use its types with another prefix (in particular for AngularJS, ng vs angular)

We can work around this by removing those export as namespace statements in our vendor'ed copy of DefinitelyTyped, but at least for us, this feature is kind of working against code maintainability and our engineering goals. I think the problems are more pronounced in a monorepo situation like Google, but generally also apply in smaller code bases.

The points that you have posted are absolutely immaterial to our situation. We are forced to implement our own code with AMD modules and supply our dependencies with UMD modules due to circumstances that are broadly outside of our control (but I would summarise as JS modules being horribly flawed in both concept and implementation). This feature would allow us to simplify our lives considerably.

Possibly with TS3 we could figure out how to avoid this, but even if we did, it would probably be at least two years before we are finished all the necessary changes, so this would still be a very useful feature for us.

Open question: Would one global flag for "Allow access to all UMD modules" be sufficient, or do people really need per-module control over the error?

Vote โค๏ธ for "just one flag"
Vote ๐ŸŽ‰ for "need per-module control"

I was also considering if the presence of an explicit list in the "types" option in tsconfig.json shouldn't also allow the UMD usage in a module. It does signify the type is present deliberately. (Though obviously doesn't preclude the error that you forgot to import it).

Or similarly, using a /// <reference types="..." /> construct should allow UMD usage of that package in the module it's used in (i.e. the 'per module control' mentioned).

@RyanCavanaugh Is there going to be a flag to address #26233 as well?

#26233 is considered fully working as intended; accessing the type side of a UMD global from a module is legitimately harmless

I am not quite sure if it is "legitimately harmless". Using @types/jquery as an example. $ and jQuery are mapped to the JQueryStatic interface and exported as constants. As a result, all modules can access $ and jQuery without an import. I hope I can disable that.

@RyanCavanaugh yes, it's harmless in the sense that the TS emit does not get affected by it. It is problematic if you want fine grained control over what @types each library can access - it's turning what at least looks like module scoped types into global types. Widening access can be a problem, too, even if emit is not affected.

Actually in the jQuery case, emit is affected. $() is emitted in a module without an import.

Accepting PRs for a new flag that allows access to UMD globals from all modules.

Implementation-wise, it's quite straightforward... but we do need to name it. We kicked around a dozen terrible names at the suggestion review meeting and hated all of them, so it's up to y'all to come up with something palatable. Please halp.

We kicked around a dozen terrible names at the suggestion review meeting and hated all of them

What were they?

so it's up to y'all to come up with something palatable.

Maybe umdUseGlobal or something.

I would suggest importAllNamespaces for the name of UMD global flag because UMD globals are usually export as namespace.

@RyanCavanaugh Did the team discuss about the type issue?

@saschanaz Asked this already, but I'm also curious... @RyanCavanaugh Do you remember what terrible names were discussed?

I think the chain went something like this

  • allowUmdGlobalAccessFromModules - most precise but soooooo long
  • assumeGlobalUmd - ugh
  • allowModuleUmdGlobals - "globals" ??
  • umdAlwaysGlobal - ๐Ÿคข
  • allowUmdGlobals - but I already can?
  • allowUmdGlobalAccess - skips the module part but probably no one cares?

I would choose the last one if forced to

Thank you!

I like allowUmdGlobalAccessFromModules the best because, although it is long, its precision makes it easiest to remember. I would think, "What's that option that allows UMD globals to be accessed from modules? Oh yeah, it's allowUmdGlobalAccessFromModules, of course!"

Using the prefix "allow" matches the naming convention of other options, which is good.

Plus... there are other options that are about as long :)

allowUmdGlobalAccessFromModules : 31 characters

allowSyntheticDefaultImports : 28 characters
strictPropertyInitialization : 28 characters
suppressExcessPropertyErrors : 28 characters
suppressImplicitAnyIndexErrors : 30 characters
forceConsistentCasingInFileNames : 32 characters

What is the current workaround? I have been googling for the past hour and can't find any workable solutions.
I'd rather not cast to 'any' or downgrade to a working Typescript version but can't find any other options.
Is there an experimental build somewhere that has a compiler flag that fixes this issue?
(by the way, 'allowUmdGlobalAccessFromModules' is an excellent name; it's not like we'd be typing it 50 times a day :-) )

We're using tsc 3.2.2 with lodash statically included in the top HTML file; with require.js; d.ts obtained from latest DefinitelyTyped; example code that fails to compile:

/// <reference path="..." />

class Example<T extends IThingWithTitle<T>> {

    public test = (arg : T[]) : void => {
        _.sortBy(arg, (el : T) => { return el.title; }); // TS2686: '_' refers to a UMD global, but the current file is a module. Consider adding an import instead.
    };

}

export = Example;

(please don't tell me I need to turn the project upside down, I know we're behind the curve in some aspects)

Update: ((window)._)/* FIXME #10178 */.sortBy(...) works but dear Lord, it is ugly :-P

@Gilead, solution from this comment works just fine for now: #10178 (comment)

Is there any progress on this? I have a case where the mentioned work-around doesn't seem to work (using typescript@3.2.2) because I'm running into this issue.


First I tried this:

import 'firebase';

declare global {
  const firebase;
}

This implicitly gives the global firebase the type any, and then applies the namespace (with the same name) to it. First this seemed to work because it shows the proper tooltips/intellisense for all the top-level keys of firebase.

However it doesn't actually work (I assume because it then switches to using it as type any, which might be a bug?):


So I tried the workaround mentioned here without success (it works for others though):

import _firebase from 'firebase'; // same with = require('firebase') 

declare global {
  const firebase: typeof _firebase;
}

=> 'firebase' is referenced directly or indirectly in its own type annotation. [2502]
(even though the namespace is aliased?)


I also tried

import * as _firebase from 'firebase';

declare global {
  const firebase: typeof _firebase;
}

=> Circular definition of import alias '_firebase'. [2303]
(maybe because of export = firebase; export as namespace firebase; in its definition?)


And finally, if I just do the import 'firebase', I'm back at

'firebase' refers to a UMD global, but the current file is a module. Consider adding an import instead. [2686]


If anyone has a solution for this, it would be very appreciated. Otherwise any of the suggestions to solve this that were mentioned so far seems fine to me really (flag, triple slash reference, types in tsconfig, having a global or external object in tsconfig).

Re @aluanhaddad's comment

This can't let all this have been in vein and go back to globals!

I'm not trying to go back to globals, I'm just trying to have a couple heavy dependencies load separately from my app bundle, while still using modules for everything else, because it brings a few benefits: the dependencies can be cached properly because they don't get updated as often as my app bundle; my bundle size doesn't explode (sometimes because of code-splitting the bundler includes the same dependency multiple times), which means less downloading for my app users; rebuilding my bundles during dev is way faster.

This is really a very easy feature to add; it'd be great for a community member to take it up.

@RyanCavanaugh I looked into it and kind of figured out I have to add the option to compiler/types.ts and modify the umd global check in compiler/checker.ts, and i think I need to add it to compiler/commandLineParser.ts as well... but I think it would take me quite a while to get it done because I'm not familiar with the source at all (like how do I add a description for the CLI flag without breaking the i18n). For now I'm going to wait for someone else who knows the source already to take it on.

@simonhaenisch You can declare it in a non-module declaration file, and to avoid of circular reference, you can re-export it in another UMD module declaration. The original firebase is declared as a namespace, lucky for us, it will augment our declaration, not causing error.

// umd.d.ts
import firebase = require("firebase");
export import firebase = firebase;
export as namespace UMD;

// global.d.ts
declare const firebase: typeof UMD.firebase;

Unfortunately, what we declared is a value, not a namespace, so you can't do something like let x: firebase.SomeInterface, the only way to alias a namespace is by declaring an import, but you can't declare import firebase = UMD.firebase;, because namespace won't augment it. Sure we can use a different name for only namspace using in type, but that will cause confusion, I'd rather drop the rest of the code I talked above, assign it to a global value at runtime, make the import alias really work.

Similar to the previous comment, we are lazy-loading hls.js (UMD) and reference types thus:

In hls.d.ts:

import * as Hls from 'hls.js';
declare global {
    const Hls: typeof Hls;
}

In the .ts file using the lazy-loaded UMD module:

/// <reference path="hls.d.ts" />
// now use it
if(Hls.isSupported()){
 ...
} 

Tested in Typescript >= 3.0.1 and 3.4.1.

Rationale is incomplete browser support for dynamic module imports.

@MatthiasHild Can it be done without the /// <reference path="hls.d.ts" /> comment?

EDIT, yes it can, as long as the declaration is within an included .d.ts file that does NOT have the same name as another .ts file, based on this SO question (that's what was getting me and why I asked).