Если вы предпочитаете 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 свойства:
Обратите внимание, что мы «расширили» тип 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>
)
Как и в случае с интерфейсами, перечисления позволяют определять набор связанных констант как часть единой сущности.
//...
/** Набор сгруппированных констант */
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 для компонентов. Тем не менее, благодаря дополнительному синтаксису, доступному в 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-элементы. Например, «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:
Преимущество 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
.
Перевод статьи Fernando Doglio: React TypeScript: Basics and Best Practices
Комментарии