Jesse
Jesse

Reputation: 6995

Define global namespace/variable within a TypeScript module

In TypeScript I can define a global/top-level namespace and use it in another file like so:

namespace foo.blah {
    export function baz() {
        console.log('hello');
    }
}

And in another file, this works:

foo.blah.baz();

However, if I then import something from another module:

import Thing from 'boo';

namespace foo.blah {
    export function baz() {
        console.log('hello');
    }
}

Suddenly my whole file is a module, the foo.blah namespace is local, not global, and the foo.blah.baz() call in the other file fails. export namespace ... just causes the namespace to become part of the module's export, not a global.

Is there a way to write a TypeScript file which imports from other modules but also defines global/top-level symbols?

At the moment I'm doing this:

import Thing from 'boo';
import * as $ from 'jquery';

namespace foo.blah {
    export function baz() {
        console.log('hello');
    }
}

$.extend(true, window, {foo});

Which works, but the TypeScript compiler still can't see that foo.blah... exists as a global in other files.

(The file is the entry point for Webpack, and I'm trying to import things from other modules in the Webpack bundle and assign them to globals so they can be used in <script> tags on the page.)

Upvotes: 17

Views: 22661

Answers (5)

Janis Veinbergs
Janis Veinbergs

Reputation: 6988

All of this is actually not recommended and red flags per documentation. Moreover it says that:

Multiple files that have the same export namespace Foo { at top-level (don’t think that these are going to combine into one Foo!)

That being said, I also had to preserve namespaced construct when migrating to ES6 modules because some systems don't offer a simple way to suddenly change namespace references like find & replace (MS Power Apps for example) or maybe you are really developing a library and want to preserve the entry point foo

If you had different first part of namespace per file, this would be enought:

(window as any).foo = foo

But having multiple, each file would overwrite previous window.foo

But there are ways what you can do when migrating to modules. With some webpack configuration, namespaces CAN be exposed as a public variables.

/* 
  Merge foonamespace between multiple files and expose it as a global variable. Otherwise foo namespace stays as module scoped variable.

  exports global variable foo (because library.name is foo), and it uses a construct that:
  - Copy the return value to a target object if it exists, otherwise create the target object first: Basically copies foo.* to foo object. This turns out to be a merge of namespaces.
  - Namespace MUST BE exported.
  - export: 'foo' means that instead of having global variable foo.foo.blah I have foo.blah (basically this should be the value whatever namespace starts with) 

*/
output: {
  ...
  library: {
    name: 'foo', //or [name] if you have namespace same as entry point name
    type: 'assign-properties' //or var or window if same namespace doesn't span multiple modules
    export: 'foo'
  },
}

There are many more ways to expose stuff to global scope (different type values available). Refer to webpack documentation

P.S. A module (having import or export statement) means variables get scoped as a module (rightly so) and that brings these challenges in the first place to preserve namespaced construct. Reference

Modules are executed within their own scope, not in the global scope; this means that variables, functions, classes, etc. declared in a module are not visible outside the module

P.S.2 How does webpack achieve it? The output looks something like:

var __webpack_export_target__ = (foo = typeof foo === "undefined" ? {} : foo);
var __webpack_exports_export__ = __webpack_exports__.foo;
for(var i in __webpack_exports_export__) __webpack_export_target__[i] = __webpack_exports_export__[i];
if(__webpack_exports_export__.__esModule) Object.defineProperty(__webpack_export_target__, "__esModule", { value: true });

Implication is second level namespace must be unique.

Upvotes: 0

David
David

Reputation: 525

For me, the problem was to call the google-one-tap library from a module. The library is only available as script, but a types package exists: @types/google-one-tap.

Code snippet from the types package @types/google-one-tap/index.d.ts:

export as namespace google;

export const accounts: accounts;

export interface accounts {
    id: {
        initialize: (idConfiguration: IdConfiguration) => void;
        disableAutoSelect: () => void;
[...]

As you can see in the above declarations file, there is a namespace google that contains objects/interfaces.

My code calling the library from a module:

google.accounts.id.initialize(...)

Without the fix (see below) the above code returns an exception when building: error TS2686: 'google' refers to a UMD global, but the current file is a module. Consider adding an import instead.

To be able to see the types and to get successful compilation, I declared a variable for the namespace google as follows. There is no code generated for declarations.

declare var google: {
  accounts: google.accounts
};

// Now this works without errors.
google.accounts.id.initialize(...)

Upvotes: 0

Acid Coder
Acid Coder

Reputation: 2747

create a global.d.ts and put your type there

reference: cannot use d.ts file to load declaration merging

Upvotes: 1

Raunak Agrawal
Raunak Agrawal

Reputation: 166

The solution is to declare namespace in global.

declare global {
  namespace foo.blah {
     function baz() {
       console.log('hello');
     }
  }
}

Upvotes: 4

MartyIX
MartyIX

Reputation: 28648

When you add the import, you switch from internal modules to external ones, as the spec says:

In external modules, relationships between files are specified in terms of imports and exports at the file level. In TypeScript, any file containing a top-level import or export is considered an external module.

http://www.typescriptlang.org/Handbook#modules-going-external

The philosophy behind external modules is to avoid global objects, why don't you create a new module with foo.blah (and all the stuff you need) and import it as TypeScript expects it?

Upvotes: 3

Related Questions