Дмитрий Нечаев
JSX в TypeScript
Введение
JSX является встраиваемым XML-подобным расширением синтаксиса JavaScript. Он должен трасформироваться в корректный JavaScript, однако семантика такого преобразования зависит от конкретной реализации. JSX завоевал популярность вместе с фреймворком React, но потом применялся и отдельно. TypeScript поддерживает встраивание, проверку типов и преобразование JSX в JavaScript напрямую.
Основы
Чтобы начать использовать JSX, необходимо сделать следующее:
- Назначить вашим файлам расширение
.tsx
- Включить опцию
jsx
TypeScript имеет три JSX режима: preserve
, react
и react-native
.
Эти режимы влияют только на стадию генерации - проверка типов не изменяется. Режим preserve
сохраняет JSX в выходном коде, который далее передаётся на следующий шаг трансформации. Выходной код получит расширение .jsx
. Режим react
сгенерирует React.createElement
, где уже не нужно трансформировать JSX перед применением, и код на выходе получит расширение .js
. Режим react-native
эквивалентен preserve
в том смысле, что он сохраняет весь JSX, но вместо этого вывод будет иметь расширение файла .js
.
Режим | Вход | Выход | Выходное расширение файла |
---|---|---|---|
preserve | .jsx | ||
react | React.createElement("div") | .js | |
react-native | .js | ||
react-jsx | _jsx("div", {}, void 0); | .js | |
react-jsxdev | _jsxDEV("div", {}, void 0, false, {...}, this); | .js |
Вы можете указать этот режим с помощью флага командной строки jsx или соответствующей опции jsx в файле tsconfig.json.
Оператор as
Вспомним, как записывается декларирование типов:
const foo = <foo>bar;
Здесь мы декларируем, что переменная bar
будет иметь тип foo
.
Так как TypeScript также использует угловые скобки для декларирования типов, синтаксис JSX's становится труднее обработать. В результате TypeScript запрещает использование угловых скобок при декларировании типов в файлах .tsx
.
Чтобы исправить эту потерю функциональности в файлах .tsx
, был добавлен новый оператор: as
.
Предыдущий пример можно переписать с использованием оператора as
.
const foo = bar as foo;
Оператор as
доступен как в .ts
так и в .tsx
файлах и ведёт себя точно также, как и другой оператор декларирования.
Проверка типов
Чтобы понять проверку типов в JSX, необходимо уяснить разницу между внутренними элементами и элементами, основанными на значении.
В JSX-выражении <expr />
, expr
может означать как внутренний элемент окружения (например, div
или span
в окружении DOM), так и созданный вами пользовательский элемент.
Это важно по следующим причинам:
- В React внутренние элементы генерируются в виде строк (
React.createElement("div")
), а пользовательские компоненты нет (React.createElement(MyComponent)
). - Типы атрибутов, передаваемых в JSX-элемент, получаются разными способами. Внутренние элементы должны быть известны по умолчанию, тогда как компоненты, скорее всего, будут создавать свои собственные наборы атрибутов.
TypeScript использует то же соглашение, что и React, чтобы различать два вышеупомянутых случая. Внутренние элементы всегда начинаются с маленькой буквы, а элементы, основанные на значении, начинаются с заглавной.
Внутренние элементы
Система находит внутренние элементы с помощью специального интерфейса JSX.IntrinsicElements
.
По умолчанию, если этот интерфейс не определён, тип всех внутренних элементов не будет проверен.
Однако если интерфейс определён, система будет искать имя внутреннего элемента как свойство интерфейса JSX.IntrinsicElements
.
Например:
declare namespace JSX {
interface IntrinsicElements {
foo: any
}
}
<foo />; // хорошо
<bar />; // ошибка
В примере выше <foo />
отработает нормально, но <bar />
приведёт к ошибке, так как он не был определён в JSX.IntrinsicElements
.
Примечание: Вы также можете определить универсальный строковый индексатор JSX.IntrinsicElements как:
declare namespace JSX { interface IntrinsicElements { [elemName: string]: any; } }
Элементы-значения
Система ищет элементы-значения по идентификаторам в пределах области видимости.
import MyComponent from "./myComponent";
<MyComponent />; // хорошо
<SomeOtherComponent />; // ошибка
Есть возможность ограничить тип элемента-значения. Но для этого необходимо ввести два новых термина: тип класса элемента (element class type) и тип экземпляра элемента (element instance type).
В выражении <Expr />
типом класса элемента является тип Expr
.
Таким образом, в вышеприведённом примере, если MyComponent
принадлежит классу ES6, типом класса будет именно этот класс.
Если MyComponent
является фабричной функцией, тип класса будет этой функцией.
Как только тип класса установлен, тип экземпляра определяется объединением возвращаемых типов сигнатуры вызова типа класса и сигнатуры конструктора. Опять же, в случае класса ES6, типом экземпляра будет тип экземпляра этого класса, а в случае фабричной функции это будет тип возвращаемого функцией значения.
class MyComponent {
render() {}
}
// использование сигнатуры конструктора
const myComponent = new MyComponent();
// element class type => MyComponent
// element instance type => { render: () => void }
function MyFactoryFunction() {
return {
render: () => {
}
}
}
// использование сигнатуры вызова
var myComponent = MyFactoryFunction();
// element class type => FactoryFunction
// element instance type => { render: () => void }
Также вызывает интерес тип элемента экземпляра, поскольку должна быть возможность назначить его JSX.ElementClass
, в противном случае возникнет ошибка. По умолчанию JSX.ElementClass
является{}
, но он может быть дополнен, чтобы ограничить использование JSX только теми типами, которые соответствуют правильному интерфейсу.
declare namespace JSX JSX {
interface ElementClass {
render: any;
}
}
class MyComponent {
render() {}
}
function MyFactoryFunction() {
return { render: () => {} }
}
<MyComponent />; // хорошо
<MyFactoryFunction />; // хорошо
class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}
<NotAValidComponent />; // ошибка
<NotAValidFactoryFunction />; // ошибка
Проверка типа атрибута
Для того, чтобы проверить типы атрибутов, сначала необходимо определить тип атрибутов элемента (element attributes type). Эта процедура немного отличается для внутренних элементов и элементов-значений.
Для внутренних элементов это тип свойства в JSX.IntrinsicElements
declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean }
}
}
// типом атрибутов элемента 'foo' является '{bar?: boolean}'
<foo bar />;
Для элементов-значений вопрос немного усложняется. Тип атрибутов определяется типом типом экземпляра элемента (element instance type), который был установлен ранее. Какое свойство использовать, определяется с помощью JSX.ElementAttributesProperty
. Оно должно быть объявлено единственным свойством, имя которого будет использоваться далее. Начиная с TypeScript 2.8, если JSX.ElementAttributesProperty
не указан, вместо него будет использоваться тип первого параметра конструктора элемента класса или вызова функционального компонента.
declare namespace JSX {
interface ElementAttributesProperty {
props; // укажите имя свойства для дальнейшего использования
}
}
class MyComponent {
// укажите свойство типа экземпляра элемента
props: {
foo?: string;
}
}
// типом атрибутов элемента 'MyComponent' является '{foo?: string}'
<MyComponent foo="bar" />
Тип атрибута элемента используется для проверки типов атрибутов в JSX. Поддерживаются опциональные и обязательные свойства.
declare namespace JSX {
interface IntrinsicElements {
foo: { requiredProp: string; optionalProp?: number }
}
}
<foo requiredProp="bar" />; // хорошо
<foo requiredProp="bar" optionalProp={0} />; // хорошо
<foo />; // ошибка, не указано requiredProp
<foo requiredProp={0} />; // ошибка, requiredProp должно быть строкой
<foo requiredProp="bar" unknownProp />; // ошибка, unknownProp не существует
<foo requiredProp="bar" some-unknown-prop />; // хорошо, потому что 'some-unknown-prop' не является корректным идентификатором
Замечание: Если имя атрибута не является корректным JS-идентификатором (как атрибут data-*), это не будет считаться ошибкой, если не будет находиться в типе атрибутов элемента.
Также можно использовать оператор расширения:
const props = { requiredProp: "bar" };
<foo {...props} />; // хорошо
const badProps = {};
<foo {...badProps} />; // ошибка
Тип результата JSX
По умолчанию результирующим типом выражения JSX является тип any
.
Вы можете изменить тип путём определения интерфейса JSX.Element
.
Однако невозможно получить информацию о типах элемента, атрибутов или потомков JSX из интерфейса. Фактически это чёрный ящик.
Встраивание выражений
JSX позволяет вставлять выражения между тегами, заключая их в фигурные скобки ({ }
).
const a = (
<div>
{["foo", "bar"].map((i) => (
<span>{i / 2}</span>
))}
</div>
);
Вышеприведённый код завершится ошибкой, так как вы не можете разделить строку на число. При использовании опции preserve
вывод будет выглядеть так:
const a = (
<div>
{["foo", "bar"].map(function (i) {
return <span>{i / 2}</span>;
})}
</div>
);
Карта развития разработчика
Получите полную карту развития разработчика по всем направлениям: frontend, backend, devops, mobile