The great split in import: clarifying the uncertainty with import in Typescript

Translation of the article prepared in advance of the start of the course “React.js Developer”


I have been working with typescript for quite some time, and I had a lot of problems with sorting out its modules and advising settings, and I must say, there really is a lot of incomprehensible around them. Namespaces import * as React from 'react', esModuleInterop etc. Therefore, let’s understand why all the hype arose.

I will not talk about namespaces as a modular system in typescript, because the idea was not the best (especially considering the current development vector), and nobody is using it now.

So, how were things before esModuleInterop? There were almost all the same modules that babel or browsers had, as well as named imports / exports. However, in matters of exports and imports by default, typescript had its own option: you had to write import * as React from 'react' (instead import React from 'react'), and, of course, here we are talking not only about react, but about all default imports from commonjs. How did it happen?

To understand this, let’s see how compatibility between some patterns in modules works. commonjs and es6. For example, we have a module that exports foo and bar as keys:

module.exports = { foo, bar }

We can import using require and destructuring:

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

And apply the same principle using named imports (although, to be honest, this is not a destructuring):

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

However, the more common pattern in commonjs – this const myModule = require('my-module') (because there was no destructuring yet), but how to do it in es6?

When developing specifications for imports in es6 one of the important aspects was compatibility with commonjssince on commonjs a lot of code has already been written. This is how the default import and export appeared. Yes, the only goal was to ensure compatibility with commonjsso that we can write import myModule from 'my-module and get exactly the same result. However, from the specifications this was not obvious, and besides, the implementation of compatibility was the prerogative of the developers of the transpiler. And here the great schism just happened: import React from 'react' or import * as React from 'react' – that is the question.

Why did typescript choose the latter? Put yourself in the place of the transpiler developer and ask yourself how to easily import imports from es6 at commonjs? Suppose you have the following set of imports and exports:

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

So, we will use the object js with a key default for export by default!

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

Cool, but what about compatibility? If import by default means we will take a field named defaultmeans when we write import React from 'react' – it will mean const { default: React } = require('react')but that doesn’t work! Then instead try using import with an asterisk. Now users will have to write import * as React from 'react'to get to the content module.exports.

However, there is a semantic difference from commonjs.
Commonjs was like regular javascript, no more. Just functions and objects, without any require. On the other hand, in import es6, require now part of the specification therefore myModule in this case, this is not just a regular javascript object, but what is called a namespace (not to be confused with namespaces in typescript), which, accordingly, has certain properties. One of them is that the namespace cannot be called. And what is the problem here, you may ask?

Let’s try another pattern. commonjs, with one function as an export:

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

We can take advantage require and execute it:

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

Although if you try to do this in a spec-complaint environment with ES6 modules, you will get an error:

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

That’s because the namespace is not the same as a javascript object, but a separate structure that stores each es6 export.

But Babel got it right and provided a compatibility option where we can write import React from 'react‘and it will work. When transiling, it marks each es6 module with a special flag in module.exportsso that we understand that if the flag is true, then it returns module.exports, and if false (for example, if it is a library commonjsthat wasn’t transpiled), then we will need to wrap the current export in { default: export }so that we can use every time default (take a look right here)

Typescript made its way through imports with asterisks, but eventually gave up and added an option esModuleInterop to the compiler. In general, this option does the same as babel, and if you enable it, you can write regular import as import React from 'react', and typescript will understand everything.

The problem is that despite the fact that in new projects it is turned on by default (when executing tsc --init), it is not suitable for existing projects (even if you upgrade to TypeScript 3), because it does not have backward compatibility. Thus, you will have to rewrite unnecessary asterisk imports to the default imports. React will treat this normally, as it is still a collection of named exports, but not for example with a namespace call. But do not be afraid if everything is in order with the export typing (and for the most part, everything is in order, since many of them fixed automatically), TypeScript 3 will allow you to quickly convert import with asterisks to standard.

So I really advocate using the option esModuleInterop, if only because it will not only allow you to write less code and make it easier to read (and these are not just words, for example, rollup will not allow you to use asterisks like this), but it also reduces the differences between the typescript and babel communities.

Warning: previously there was an option enableSyntheticDefaultImports, which silenced the compiler when it tried to complain about incorrect import by default, so we needed our own way to ensure compatibility with commonjs (eg, WebpackDefaultImportPlugin), but it was problematic, because, for example, if you have tests, then you still need to ensure such compatibility. note that esModuleInterop includes synthetic import by default only if your goal <= ES5. So if you enable this option, and compilers continue to complain about import React, then understand what goal you are pursuing, and perhaps turning on imports by default will be your option (or restarting vscode / webstorm, who knows).

I hope my explanation has clarified the situation a little, but if you still have questions, you can ask me them in twitter!


React patterns


Similar Posts

Leave a Reply