Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: import * as moment from 'moment' vs. import moment from 'moment'

Tags:

typescript

I'm having a weird issue with Typescript and importing the moment package. I've seen it in a couple of different places, and I've even seen it crop up in the same file depending on whether or not I added a specific static method to a class.

The problem is this:

When I import moment with import * as moment from 'moment';, I get no Typescript errors, but I do get an issue with my tests; specifically, they fail with TypeError: moment is not a function.

When I change the import the import moment from 'moment';, the code works, but I get "moment" has no default export.

I don't understand the difference between these. I've seen other questions (like this one, which recommends the first syntax). The a comment on the accepted answer to the question above suggests turning on the allowSyntheticDefaultImports flag, which does correct the issue, but I'm a little concerned about that flag. The documentation says:

Allow default imports from modules with no default export. This does not affect code emit, just typechecking.

which seems to imply that there is no typechecking for modules imported in this way.

So, two questions:

  1. Am I understanding the TS documentation for the allowSyntheticDefaultImports flag correctly? Is it turning off typechecking for the moment package?
  2. Is there a way to (consistently) import this package?

FWIW, I'm using Typescript 3.4.5 and moment 2.24.0.

like image 856
Kryten Avatar asked Jun 08 '19 03:06

Kryten


People also ask

How do I import a moment in TypeScript?

To import moment. js with TypeScript, we can import the whole package with import . import * as moment from "moment"; to import the 'moment' module as moment .

Does moment JS work with TypeScript?

As of version 2.13.0, Moment includes a typescript definition file. To use moment.locale you first need to import the language you are targeting.

What is allowSyntheticDefaultImports?

When set to true, allowSyntheticDefaultImports allows you to write an import like: ts. import React from "react"; instead of: ts.


1 Answers

  1. Don't worry about allowSyntheticDefaultImports, it is not what you want. You want esModuleInterop.
  2. Yes

TL;DR Turn on esModuleInterop and replace every instance of import * as foo from 'foo' with import foo from 'foo'.

The problem stems from the fact that Node's require is too flexible. You can export pretty much anything. This makes it very difficult to optimize the resulting code for reasons beyond the scope of this question.

When ES defined modules they defined something slightly more structured. You must export a plain object. This object can have a property named default but it doesn't need to.

Unfortunately, many JS packages were not following this convention. If we look at moment...

> const moment = require('moment');
undefined
> typeof moment
'function'
> moment.default
undefined

You can see that moment exports a function and it has no property named default. This is a conundrum for TS which aims to stick to the ES standard but wants to support importing old fashioned modules which don't follow the standard.

The first attempt (and the default behavior) was to break the ES standard if you are compiling to an older target and replace import * as foo from 'foo' with const foo = require('foo');. The thought was that no one really needs import * as foo from 'foo' and so this was a way to hijack that import style to hack in support for older modules. It doesn't comply with ES6 though because the return value of import * as foo from 'foo' should always be an object and if we replace it with require then we might not get an object.

Unfortunately, this leads to a situation where your code will behave differently depending on what you are targeting and how you are building. If you target ES5 then it will fallback to the require behavior and you will get back a function. If you target ES6 (which can often happen when using some kind of bundler) then it will put in import * as foo from 'foo' and the bundler may interpret that differently. What they normally do is give you an object with a single property named default set to that function.

So, the solution was to instead hijack the import moment from 'moment' syntax. In ES6 what this does is import the default property of whatever gets exported. The nice thing is that this means the thing that gets imported doesn't have to be an object and can be a function. So instead of just dropping in const moment = require('moment') TS does something a little different. It creates an object with a single property named default and sets it equal to require('moment') and then returns that.

It's really just a different hack to solve the same problem but it follows the ES6 spec and it behaves more similarly to how Babel behaves. Or, to quote the TS release notes on this feature:

Note: The new behavior is added under a flag to avoid unwarranted breaks to existing code bases. We highly recommend applying it both to new and existing projects. For existing projects, namespace imports (import * as express from "express"; express();) will need to be converted to default imports (import express from "express"; express();).

like image 149
Pace Avatar answered Oct 16 '22 19:10

Pace