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:
allowSyntheticDefaultImports
flag correctly? Is it turning off typechecking for the moment
package?FWIW, I'm using Typescript 3.4.5 and moment 2.24.0.
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 .
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.
When set to true, allowSyntheticDefaultImports allows you to write an import like: ts. import React from "react"; instead of: ts.
allowSyntheticDefaultImports
, it is not what you want. You want esModuleInterop
.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();).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With