Я довольно долго работаю с 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 = 1export const bar = 2export 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.fooconst 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