https://habr.com/ru/company/otus/blog/507104/- Блог компании OTUS. Онлайн-образование
- JavaScript
- TypeScript
- Программирование
Перевод статьи подготовлен в преддверии старта курса «Разработчик 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