Подготовка к работе
create-react-app с TypeScript
$ npx create-react-app your-app-name --template typescript
Если вы предпочитаете Yarn, используйте следующую команду:
$ yarn create react-app your-app-name --template typescript
Обратите внимание, что мы не используем приложение напрямую, а применяем инструменты, которые загружают последнюю версию приложения при необходимости.
Основы
Интерфейсы
Одним из множества преимуществ TypeScript является доступ к конструкциям, которые позволяют определять интерфейс компонентов и других сложных объектов, используемых с ними, таких как форма объекта Props
(количество свойств и их типов).
import React from 'react';
interface IButtonProps {
/** Текст внутри кнопки */
text: string,
/** Тип кнопки, извлеченный из перечисления ButtonTypes */
type: ButtonTypes,
/** Функция, выполняемая после нажатия кнопки */
action: () => void
}
const ExtendedButton : React.FC<IButtonProps> = ({text, type, action}) => {
}
В приведенный выше код необходимо добавить 3 свойства:
- text: представлен String.
- type: представлен опцией ButtonType.
- action: простая функция.
Обратите внимание, что мы «расширили» тип FC (функциональный компонент) с помощью собственного пользовательского интерфейса. Благодаря этому функция получает все общие определения функциональных компонентов, такие как prop и тип return, которые должны быть присвоены JSX.Element.
Если вы проигнорируете одно из них или отправите несовместимое значение, компилятор TypeScript и IDE (при условии, что вы используете специфичную для JavaScript IDE, такую как Code) уведомят вас об этом. Вы сможете продолжить работу только после исправления ошибки.
Лучший способ определить элемент ExtendedButton — расширить нативный тип HTML-элемента button следующим образом:
import React, {ButtonHTMLAttributes} from 'react';
interface IButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Текст внутри кнопки */
text: string,
/** Тип кнопки, извлеченный из перечисления ButtonTypes */
type: ButtonTypes,
/** Функция, выполняемая после нажатия кнопки */
action: () => void
}
const ExtendedButton : React.FC<IButtonProps> = ({text, type, action}) => {
}
Также обратите внимание, что при работе с Bit.dev или react-docgen для автоматической генерации документов потребуется следующий синтаксис:
const ExtendedButton : React.FC<IButtonProps> = ({text, type, action} : IButtonProps) => {
}
(Прямое определение props можно выполнить с помощью: IButtonProps
в дополнение к определению компонента с :React.FC<IButtonProps>
)
Перечисления (Enums)
Как и в случае с интерфейсами, перечисления позволяют определять набор связанных констант как часть единой сущности.
//...
/** Набор сгруппированных констант */
enum SelectableButtonTypes {
Important = "important",
Optional = "optional",
Irrelevant = "irrelevant"
}
interface IButtonProps {
text: string,
/** Тип кнопки, извлеченный из перечисления SelectableButtonTypes */
type: SelectableButtonTypes,
action: (selected: boolean) => void
}
const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
let [selected, setSelected] = useState(false)
return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={ _ => {
setSelected(!selected)
action(selected)
}}>{text}</button>)
}
/** Экспорт компонента и перечисления */
export { ExtendedSelectableButton, SelectableButtonTypes}
Импорт и использование перечислений:
import React from 'react';
import './App.css';
import {ExtendedSelectableButton, SelectableButtonTypes} from './components/ExtendedSelectableButton/ExtendedSelectableButton'
const App = () => {
return (
<div className="App">
<header className="App-header">
<ExtendedSelectableButton type={SelectableButtonTypes.Important} text="Select me!!" action={ (selected) => {
console.log(selected)
}} />
</header>
</div>
);
}
export default App;
Обратите внимание, что в отличие от интерфейсов или типов, перечисления переводятся на простой JavaScript. Например:
enum SelectableButtonTypes {
Important = "important",
Optional = "optional",
Irrelevant = "irrelevant"
}
Код выше преобразуется в следующее:
"use strict";
var SelectableButtonTypes;
(function (SelectableButtonTypes) {
SelectableButtonTypes["Important"] = "important";
SelectableButtonTypes["Optional"] = "optional";
SelectableButtonTypes["Irrelevant"] = "irrelevant";
})(SelectableButtonTypes || (SelectableButtonTypes = {}));
Интерфейсы и псевдонимы типов
Многие новички в TypeScript часто сталкиваются с такой проблемой, как использование интерфейсов или псевдонимов типов для различных частей кода. Официальная документация не дает точного ответа по этой теме.
Несмотря на то, что теоретически эти сущности различны, на практике они очень схожи:
- Могут расширяться.
//расширенные интерфейсы
interface PartialPointX { x: number; }
interface Point extends PartialPointX { y: number; }
//расширенные типы
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
//интерфейс расширяет тип
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
//псевдоним типов расширяет интерфейсы
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
2. Могут использоваться для определения формы объектов.
//определение интерфейса для объектов
interface Point {
x: number;
y: number;
}
//использование типов
type Point2 = {
x: number;
y: number;
};
3. Могут быть реализованы одинаково.
//реализация интерфейса
class SomePoint implements Point {
x: 1;
y: 2;
}
//реализация псевдонима типа
class SomePoint2 implements Point2 {
x: 1;
y: 2;
}
type PartialPoint = { x: number; } | { y: number; };
// Единственное, что нельзя выполнить: реализовать тип объединения.
class SomePartialPoint implements PartialPoint {
x: 1;
y: 2;
}
Единственная дополнительная функция интерфейсов — это «объединение описаний», т. е. вы можете определить один и тот же интерфейс несколько раз, и с каждым определением свойства объединяются:
interface Point { x: number; } //описание #1
interface Point { y: number; } //описание #2
// Оба описания становятся:
// interface Point { x: number; y: number; }
const point: Point = { x: 1, y: 2 };
Дополнительные типы для props
К преимуществам использования интерфейсов можно отнести возможность применять свойства props для компонентов. Тем не менее, благодаря дополнительному синтаксису, доступному в TypeScript, можно также определить дополнительные props. Например:
//...
interface IProps {
prop1: string,
prop2: number,
myFunction: () => void,
prop3?: boolean //optional prop
}
//...
function MyComponent({...props}: IProps) {
//...
}
/** Затем их можно использовать следующим образом */
<mycomponent prop1="text here" prop2=404 myFunction={() = {
//...
}} />
<mycomponent prop1="text here" prop2={404} myFunction={() = {
//...
}} prop3={false} />
Хуки
Хуки — это новый механизм React для взаимодействия с некоторыми функциями (например, состоянием) без необходимости определять класс.
Добавление проверки типа в хуки
Такие хуки, как useState
, получают параметр, возвращают состояние и функцию для его установки.
Благодаря проверке типа в TypeScript можно реализовать тип (или интерфейс) начального значения состояния следующим образом:
const [user, setUser] = React.useState<IUser>(user);
Нулевые значения для хуков
Если начальное значение для хука потенциально может быть null
, то в приведенном выше примере возникнет ошибка. В этих случаях TypeScript также позволяет установить дополнительный тип для защиты со всех сторон.
const [user, setUser] = React.useState<IUser | null>(null);
// позже...
setUser(newUser);
Таким образом, вы не только сохраняете проверки типов, но и учитываете те сценарии, в которых начальное значение может быть равно null
.
Общие компоненты
Подобно общим функциям и интерфейсам в TypeScript, можно определять и общие компоненты, чтобы использовать их повторно для разных типов данных. То же самое можно сделать с props и состояниями.
interface Props<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>(props: Props<T>) {
const { items, renderItem } = props;
const [state, setState] = React.useState<T[]>([]);
return (
<div>
{items.map(renderItem)}
</div>
);
}
Затем компонент можно использовать либо с помощью вывода типа, либо напрямую указав типы данных.
Пример вывода типа:
ReactDOM.render(
<List
items={["a", "b"]} // выведенный тип 'string'
renderItem={item => (
<li key={item}>
{item.trim()} //допустимо, поскольку мы работаем с 'strings'
</li>
)}
/>,
document.body
);
Объявленные напрямую типы:
ReactDOM.render(
<List<number>
items={[1,2,3,4]}
renderItem={item => <li key={item}>{item.toPrecision(3)}</li>}
/>,
document.body
);
Обратите внимание: если в последнем примере список содержит строки вместо цифр, то TypeScript выдает ошибку во время процесса транспиляции.
Расширение HTML-элементов
Иногда компоненты функционируют и ведут себя как нативные HTML-элементы. Например, «border-box» (компонент, который отображает div
с рамкой по умолчанию) или «big submit» (старая добрая кнопка отправки с размером по умолчанию и некоторым пользовательским поведением).
Для этих сценариев лучше всего определить тип компонента как нативный HTML-элемент или его расширение.
export interface IBorderedBoxProps extends React.HTMLAttributes<HTMLDivElement> {
title: string;
}
class BorderedBox extends React.Component<IBorderedBoxProps, void> {
public render() {
const {children, title, ...divAttributes} = this.props;
return (
//Это DIV, и мы пытаемся предоставить пользователю эту информацию.
<div {...divAttributes} style={{border: "1px solid red"}}>
<h1>{title}</h1>
{children}
</div>
);
}
}
const myBorderedBox = <BorderedBox title="Hello" onClick={() => alert("Hello")}/>;
В коде выше я расширил HTML props по умолчанию и добавил новое: «title».
Типы событий
React предоставляет собственный набор событий, поэтому использовать старые добрые HTML-события напрямую не получится. При этом у вас есть доступ ко всем необходимым UI-событиям. Они имеют те же имена, поэтому убедитесь, что ссылаетесь на них напрямую, как React.MouseEvent
, или просто импортируйте их из React:
import React, { Component, MouseEvent } from 'react';
Преимущество TypeScript в данном случае заключается в возможности использовать Generics (как в предыдущем примере), чтобы ограничить элементы, на которых может использоваться обработчик событий.
Например, следующий код не будет работать:
function eventHandler(event: React.MouseEvent<HTMLAnchorElement>) {
console.log("TEST!")
}
const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
let [selected, setSelected] = useState(false)
return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={eventHandler}>{text}</button>)
}
И вы получите подобное сообщение об ошибке:
Однако вы можете использовать объединения, чтобы разрешить повторное использование одного обработчика несколькими компонентами:
/** Благодаря этому вы сможете использовать обработчик событий как для элементов anchors, так и для button */
function eventHandler(event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) {
console.log("TEST!")
}
Интегрированное определение типа
В качестве последнего совета стоит упомянуть файлы index.d.ts и global.d.ts. Они устанавливаются при добавлении React в проект (если вы использовали npm, вы найдете их в папке npm_modules/@types).
Эти файлы содержат определения типов и интерфейсов, используемых React. Если вам нужно разобраться в props определенного типа, просто откройте эти файлы и просмотрите их содержимое.
Например:
Там вы можете увидеть небольшой раздел файла index.d.ts, в котором показаны различные подписи для функции createElement
.
Читайте также:
- Конец эпохи виртуального DOM (React)
- Первые шаги в анимации React Native
- Как предотвратить состояние гонки с помощью React Context API
Перевод статьи Fernando Doglio: React TypeScript: Basics and Best Practices