Перевод статьи подготовлен в преддверии старта курса «Разработчик React.js»




Я довольно долго работаю с typescript, и у меня было много проблем с тем, чтобы разобраться с его модулями и советующими настройками, и должен сказать, вокруг них и вправду много непонятного. Пространства имен, import * as React from 'react', esModuleInterop и т.д. Поэтому давайте разберемся из-за чего поднялась вся шумиха.

Я не буду говорить о пространствах имен как о модульной системе в typescript, поскольку идея оказалась не лучшей (особенно учитывая текущий вектор развития), и этим никто сейчас не пользуется.

Итак, как же обстояли дела до появления esModuleInterop? Были почти все те же модули, что есть у babel или браузеров, а также именованные импорты/экспорты. Однако, в вопросах экспортов и импортов по умолчанию у typescript был свой собственный вариант: нужно было писать import * as React from 'react' (вместо import React from 'react'), и, конечно, здесь речь не только о react, а обо всех импортах по умолчанию из commonjs. Как так вышло?

Чтобы в этом разобраться, давайте посмотрим, как работает совместимость между некоторыми паттернами в модулях commonjs и es6. Например, у нас есть модуль, который экспортирует foo и bar в качестве ключей:

module.exports = { foo, bar }

Мы можем сделать импорт с помощью require и деструктуризации:

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

И применить тот же принцип, используя именованный импорт (хотя, если по-честному, то это не деструктуризация):

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

Однако более распространенный паттерн в commonjs – это const myModule = require('my-module') (потому, что деструктуризации еще не было), но как сделать это же в es6?

При разработке спецификации для импорта в es6 одним из важных аспектов была совместимость с commonjs, так как на commonjs было уже написано много кода. Вот так и появились импорт и экспорт по умолчанию. Да, единственной целью было обеспечивать совместимость с commonjs, чтобы мы могли писать import myModule from 'my-module и получать ровно тот же результат. Однако из спецификаций это было неочевидно, и к тому же, реализация совместимости была прерогативой разработчиков транспайлера. И вот тут как раз и случился великий раскол: import React from 'react' или же import * as React from 'react' – вот в чем вопрос.

Почему typescript выбрал последнее? Поставьте себя на место разработчика транспайлера и спросите себя, как можно легче всего транспилировать импорты из es6 в commonjs? Допустим, у вас есть следующий набор импортов и экспортов:

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

Итак, будем использовать объект js с ключом default для экспорта по умолчанию!

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

Круто, но как насчет совместимости? Если импорт по умолчанию означает, что мы возьмем поле с именем default, значит когда мы напишем import React from 'react' – это будет значить const { default: React } = require('react'), но так не работает! Тогда вместо этого попробуем использовать импорт со звездочкой. Теперь пользователям придется писать import * as React from 'react', чтобы добраться до содержимого module.exports.

Однако здесь есть семантическое отличие от commonjs.
Commonjs был как обычный javascript, не больше. Просто функции и объекты, без всяких require. С другой стороны, в импорте es6, require сейчас часть спецификации, поэтому myModule в данном случае – это не просто обычный объект javascript, а то, что зовется пространством имен (не путать с namespaces в typescript), которое, соответственно, обладает определенными свойствами. Одно из них заключается в том, что пространство имен нельзя вызвать. И в чем же тут проблема, вы можете спросить?

Давайте опробуем другой паттерн commonjs, с одной функцией в качестве экспорта:

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

Мы можем воспользоваться require и выполнить ее:

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

Хотя если попытаетесь выполнить это в spec-complaint среде с модулями ES6, то получите ошибку:

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

Все потому, что пространство имен – это не то же самое, что объект javascript, а отдельная структура, хранящая каждый экспорт es6.

Но вот Babel понял все правильно и предоставил такой вариант совместимости, при котором мы можем написать import React from 'react' и это будет работать. При транспиляции он помечает каждый модуль es6 специальным флагом в module.exports, чтобы мы понимали, что если флаг истинный, то возвращается module.exports, а если ложный (например, если это библиотека commonjs, которая не была транспилирована), то нам нужно будет обернуть текущий экспорт в { default: export }, чтобы мы могли каждый раз использовать default (взгляните вот сюда).

Typescript пробивался за счет импортов со звездочками, но в итоге сдался и добавил опцию esModuleInterop в компилятор. В целом, эта опция делает то же самое, что и babel, и если вы ее включите, то можете написать обычный импорт как import React from 'react', и typescript все поймет.

Проблема в том, что несмотря на то, что в новых проектах она включается по умолчанию (при выполнении tsc --init), она не подойдет для уже существующих проектов (даже если вы обновитесь до TypeScript 3), потому что у нее нет обратной совместимости. Таким образом вам придется переписать ненужные импорты со звездочками на импорты по умолчанию. React отнесется к этому нормально, поскольку это все еще набор именованных экспортов, но не к примеру с вызовом пространства имен. Но не бойтесь, если с типизацией экспортов все в порядке (а с ними в большинстве своем все в порядке, поскольку множество из них исправляется автоматически), TypeScript 3 позволит вам быстро преобразовать импорт со звездочками к стандартному.

Поэтому я действительно выступаю за использование опции esModuleInterop, хотя бы потому что она не только позволят вам писать меньше кода и облегчает его чтение (и это не просто слова, например, rollup не позволит вам так использовать импорты со звездочками), но и уменьшает разногласия между сообществами typescript и babel.

Предостережение: раньше существовала опция enableSyntheticDefaultImports, которая затыкала рот компилятору, когда он пытался пожаловаться на неправильный импорт по умолчанию, поэтому нам понадобился собственный способ обеспечивать совместимость с commonjs (например, WebpackDefaultImportPlugin), но это было проблемно, поскольку, например, если у вас есть тесты, то вам все еще нужно обеспечивать такую совместимость. Обратите внимание, что esModuleInterop включает синтетический импорт по умолчанию только в случае, если ваш цель <= ES5. Поэтому если вы включите эту опцию, а компиляторы продолжат жаловаться на import React, то поймите, какую цель вы преследуете, и, возможно, включение импортов по умолчанию будет вашим вариантом (или же перезапуск vscode/webstorm, кто знает).

Надеюсь, мое объяснение хоть немного прояснило ситуацию, но если у вас остались вопросы, вы можете задать мне их в twitter!



React Patterns