1

If a module (e.g. moment.js, knockout, or big.js) is included with a <script> tag e.g.

<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.js">
</script>

that define a global property (e.g. moment, ko, Big, etc), how can one access/declare the types on window (or global) in Typescript.

For example

const x = moment()
const t = ko.observable()
const b = new Big()

How can one set the ambient type of these globals without including the entire moment.js library? The objective is to have properly typed global references to be used by VS Code, and tsc, ts-loader, or babel-typescript.

In the case of moment, the types are exposed at node_modules/moment/moment.d.ts, but for other libraries (e.g. knockout or big.js) they're at @types/[module]/index.d.ts.

It feels like this would be quite common, but I haven't seen a good, working reference of how to accomplish this.


Here is the tsconfig:

{
    "compilerOptions": {
      "target": "ESNext",
      "moduleResolution": "node",
      "allowJs": true,
      "noEmit": true,
      "strict": false,
      "isolatedModules": false,
      "esModuleInterop": true,
      "noResolve": false,
      "baseUrl": ".",
      "paths": {
        "*": [
          "*",
          "js.packages/*"
        ]
      },
      "jsx": "preserve",
      "outDir": "dist/"
  },
  "include": [
    "js.packages/**/*.ts",
    "js.packages/**/*.tsx",
    "js.packages/@types/lib.d.ts",
  ],
  "files": [
    "services/app/src/entry.js"
  ],
  "exclude": [
    "node_modules"
  ]
}

Here's a lib.d.ts

declare global {
  type Bigable = Big | string | number
  interface Window {
    Big: typeof import('big.js'),
    moment: typeof import('moment'),
    Sentry: typeof import('@sentry/browser'),
  }
}

and here's how consumption should work:

const v = new Big(1)
const m = moment()
const s = global.Sentry()
const o = ko.observable()

which looks like this in VS Code (with the red underline indicating failures):

failing types

So knockout works because @types/knockout/index.d.ts has:

declare var ko: KnockoutStatic;
declare module "knockout" {
    export = ko;
}

whereas I've similarly declared a global Big on interface Window.

Unfortunately Sentry and moment (in this example) do not seem to work, and it's unclear why or what one might have to do to fix this.

9
  • Isn't this the same as stackoverflow.com/questions/55778938/… ? Commented May 20, 2019 at 21:10
  • Since all of the types are UMD declarations, and since TypeScript automatically looks in both node_modules/[package] and node_modules/@types/[package] you shouldn't need to do anything in particular. Please post your tsconfig.json. It's perfectly possible to install the moment package just to get the types while still loading the implementation from a script tag at runtime. Commented May 20, 2019 at 22:12
  • @AluanHaddad I've posted quite a few more details, which I hope help illuminate. Commented May 21, 2019 at 0:35
  • @TitianCernicova-Dragomir Thanks; that's a great question too, and you can see specifics from edits I've made how the answers in that question fall short. Commented May 21, 2019 at 0:36
  • Can you please provide an example of what it is you’d like to do (I.e. usage)? Commented May 21, 2019 at 0:48

2 Answers 2

8
+250

You have the right idea, you need to use an import type to get the types in an import, you need to declare variables in global. The problem is that you are declaring the properties on Window. While any key assigned to Window becomes a global variable typescript does not reflect this. You can declare the variables directly in the global scope.

declare global {
  export var moment: typeof import('moment');
  export var Sentry: typeof import('@sentry/browser');
}

const m = moment()
Sentry.init({}) // Sentry() does not seem like it should work according to docs
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, that works. Is this spelled out in the docs or anywhere else?
@BrianM.Hunt Not entirely sure .. there are docs about declaration merging typescriptlang.org/docs/handbook/declaration-merging.html which mention global augmentation. But the specific issue with Window I'm not sure is mentioned in the docs (maybe some GH issue)
0

The question is not terribly specific to one type of platform, but I'll tell you how I get moment working in Angular.

First, you shouldn't be loading from a script URL. You should run npm install moment to put it in your node_modules folder. This will ensure it gets packed up into the minified version of your application. You don't have to, but this is strongly recommended.

"scripts": [
          "node_modules/jquery/dist/jquery.slim.min.js",
          "node_modules/moment/moment.js",
           ...
        ]

Regardless of the above, once you have the script linked in your tsconfig (or angular.json), usage is as simple as the following line at the top of the file:

import * as moment from 'moment';

I tend to use this syntax for most libraries which have an object, e.g. shortid, linq, and lodash.

Other libraries work better with declare var statements. For example, I've found jQuery to work better that way.

declare var $

Autocomplete

If you're looking for autocomplete, you need to have a .d.ts file loaded for the library. The npm package for moment comes with one, but some libraries need to have another npm package installed for this (e.g. @types/lodash, @types/jquery).

Visual Studio Autocomplete relies upon .d.ts files to identify types. The documentation for configuring this feature is located here, and it may be possible to configure it (see the note near the bottom).

10 Comments

Thanks @theMayor; I've used <script> tags for importing to simplify the question because what we're actually doing is webpack-chunks for globals for cache re-use across the browser main event loop, and web- and service-workers. These libs effectively appear as globals, just like a script tag. All that is somewhat beside the typescript point (we include 50+ JS libraries totalling tens of MB so caching/chunking is important to performance); the key element is we don't want to import a copy of every library in the app for every worker, which is what import * as moment from 'moment' does?
@BrianM.Hunt - I'm not sure I understand what you're trying to accomplish - doesn't webpack do this automatically? In any case, you're probably foregoing any type checking that VS Code may have to offer by doing so, and thus, yielding most, if not all, the benefit of Typescript in the first place.
Type-checking does not necessitate inclusion, much the way in C/C++ a .h file can provide the types of an external library.
But, what if you miss something? In any case, the question should be reworded to ask "How do I get intellisense without importing the library?" as that would have been a lot clearer from the get-go. Brief research indicates it is possible.
Yeah, I mean, this is sort of the age-old frustration with JavaScript. It seems to be true that something meant to attach itself to the global scope is "old-school" and thus, maybe it just isn't a thing that the folks who put typescript together gave much thought to?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.