Great import schism: Typescript confusion around imports explained

Mikhail Bashurov
ITNEXT
Published in
5 min readNov 13, 2018

--

I’ve been working with typescript for quite a while and had a decent amount of problems understanding its modules and correspondent settings and I gotta say that there’s a lot of confusion around it. Namespaces, import * as React from 'react', esModuleInterop and so on. So let me try to explain what the fuss is all about.

I won’t talk about namespaces as a module system in typescript cause the idea turned out to be not so good (at least considering the current direction of development) and nobody use them at the moment.

So, what did we have before esModuleInterop option? We had almost the same modules as babel or browsers have, especially considering named imports/exports. But in regards to default exports and imports typescript had it's own way of doing things, we had to write import * as React from 'react' (instead of import React from 'react') and, of course, i'm not talking only about react, but about all default imports from commonjs library and why is that?

To figure it out let’s see how interoperability works between some patterns in commonjs and es6 modules. For example, we have a module that export foo and bar as keyed object:

module.exports = { foo, bar }

We can import it using require and destructuring:

const { foo, bar } = require('my-module')

And apply the same principle using named imports (though it’s not destructuring to be fair):

import { foo, bar } from 'my-module'

But more common pattern in commonjs code is const myModule = require('my-module') (cause we didn't have destructuring yet) so how do we do this in es6?

When developing a spec for es6 import, one of the important questions was interoperability with commonjs, since there was already a lot of code in commonjs. So this is how default imports/exports were born. Yes. Their sole purpose was to provide an interop with commonjs, so we could write import myModule from 'my-module' and get the same thing. But it wasn't clear from the spec and plus, interop implementation was transpiler's developers prerogative. So here happened the great schism: import React from 'react' vs import * as React from 'react'.

Why did typescript picked the latter? Imagine yourself as a transpiler developer and ask, what will be the easiest way to transpile es6 imports to commonjs?. Imagine we have a following set of exports and imports:

export const foo = 1
export const bar = 2
export default () => {}
import { foo } from 'module'
import func from 'module'`

So let’s use js object with a default key for the default export!

module.exports = {
foo: 1,
bar: 2,
default: () => {}
}
const module = require('module')
const foo = module.foo
const func = module.default

Ok, cool, but what about interop? If the default import means taking field named default then when we write import React from 'react' it means const { default: React } = require('react'), but it's not gonna work! Let's use star imports instead, so the users will have to write import * as React from 'react' to get whatever is lying in the module.exports.

But there’s a semantic difference here from the commonjs. Commonjs was, like, plain javascript, no more. Just functions and objects, there was no require in the js spec. ES6 import, on the other hand, is a part of the spec right now, so myModule in this case isn't just a plain javascript object, but a thing called a namespace (not typescript namespaces) and therefore has a certain properties to it. One of them is that namespace isn't callable. How is that a problem, one might ask?

Let's try another commonjs pattern, with a single function as an export:

module.exports = function() { // do something }

We can require and execute it:

const foo = require('my-module')
foo()

Though if will try to do this in the spec-complaint environment with the ES6 modules you’ll get and error:

import * as foo from 'my-module'
foo() // Error

Cause the namespace is not the same as just javascript object, but a specific thing for containing every es6 export.

But Babel got it right and presented some sort of interop so we can write import React from 'react' and it will work. What it does is when transpiling it marks every es6 module with a special flag in the module.exports so we can see whether this flag is truthy and returnmodule.exports or it’s falsy (obviously if library is commonjs and wasn’t transpiled) and then we’re gonna wrap current export in { default: export } so we can just use default every time (see this).

Typescript fought its way with star imports, but eventually gave up and introduced esModuleInterop compiler option. This option is basically does the same thing as babel and if you'll enable it you can write usual import React from 'react' and typescript will do the job.

Problem is that, though it's enabled by default for the new projects (when you run tsc --init), it's not for the existing projects (even if you update to TS 3), because it's not backwards compatible, so you'll have to rewrite your unneeded star imports in favor of default ones. React will be fine since it a bunch of named exports, but the example with callable namespace won’t. But fear not, if the typings are correct about exports (and they're mostly correct since they fixed a lot of them automatically) TS 3 will provide you a quick fix to convert a star import to default.

So I really advocate the usage of esModuleInterop option, cause it not only allows you to write less code, easier to read and spec-complaint code (and it's not just ranting, rollup for example won't allow you to use star imports in such way), but also mitigates some friction between typescript and babel communities.

Caveat: previously there was enableSyntheticDefaultImports option which basically shut the compiler up about incorrect default imports, so we would have to provide our own way of interop with commonjs (WebpackDefaultImportPlugin for example), though there was quite an amount of issues with that such as if you have tests you also need to provide this interop. The caveat is that esModuleInterop enables synthetic default imports automatically only if target is <= ES5. So if you you'll enable this option, but the compilers will still complain about import React , check your target and maybe enable synthetic default import (or restart your vs code/webstorm, you never know :).

I hope the explanation was clear for you, but if you have any questions feel free to ask them in the comments or on twitter!

--

--