Перевод статьи подготовлен в преддверии старта курса «Разработчик React.js»
Я довольно долго работаю с typescript, и у меня было много проблем с тем, чтобы разобраться с его модулями и советующими настройками, и должен сказать, вокруг них и вправду много непонятного. Пространства имен,
Я не буду говорить о пространствах имен как о модульной системе в typescript, поскольку идея оказалась не лучшей (особенно учитывая текущий вектор развития), и этим никто сейчас не пользуется.
Итак, как же обстояли дела до появления
Чтобы в этом разобраться, давайте посмотрим, как работает совместимость между некоторыми паттернами в модулях
Мы можем сделать импорт с помощью require и деструктуризации:
И применить тот же принцип, используя именованный импорт (хотя, если по-честному, то это не деструктуризация):
Однако более распространенный паттерн в commonjs – это
При разработке спецификации для импорта в
Почему typescript выбрал последнее? Поставьте себя на место разработчика транспайлера и спросите себя, как можно легче всего транспилировать импорты из
Итак, будем использовать объект
Круто, но как насчет совместимости? Если импорт по умолчанию означает, что мы возьмем поле с именем
Однако здесь есть семантическое отличие от
Давайте опробуем другой паттерн
Мы можем воспользоваться
Хотя если попытаетесь выполнить это в spec-complaint среде с модулями ES6, то получите ошибку:
Все потому, что пространство имен – это не то же самое, что объект javascript, а отдельная структура, хранящая каждый экспорт es6.
Но вот Babel понял все правильно и предоставил такой вариант совместимости, при котором мы можем написать
Typescript пробивался за счет импортов со звездочками, но в итоге сдался и добавил опцию
Проблема в том, что несмотря на то, что в новых проектах она включается по умолчанию (при выполнении
Поэтому я действительно выступаю за использование опции
Предостережение: раньше существовала опция
Надеюсь, мое объяснение хоть немного прояснило ситуацию, но если у вас остались вопросы, вы можете задать мне их в twitter!
React Patterns
Я довольно долго работаю с 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