иконка discount

Скидка 10% по промокоду

новогодняя распродажа до 05.01.2025иконка discount
NEWYEAR2025
логотип PurpleSchool
Иконка входа
Вход
логотип PurpleSchool

JSX в TypeScript

Автор

Дмитрий Нечаев

Введение

JSX является встраиваемым XML-подобным расширением синтаксиса JavaScript. Он должен трасформироваться в корректный JavaScript, однако семантика такого преобразования зависит от конкретной реализации. JSX завоевал популярность вместе с фреймворком React, но потом применялся и отдельно. TypeScript поддерживает встраивание, проверку типов и преобразование JSX в JavaScript напрямую.

Основы

Чтобы начать использовать JSX, необходимо сделать следующее:

  1. Назначить вашим файлам расширение .tsx
  2. Включить опцию 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), так и созданный вами пользовательский элемент. Это важно по следующим причинам:

  1. В React внутренние элементы генерируются в виде строк (React.createElement("div")), а пользовательские компоненты нет (React.createElement(MyComponent)).
  2. Типы атрибутов, передаваемых в 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