Статическая типизация в React приложении

Автор оригинала: Дмитрий Котляренко (Dmitry Kotlyarenko)
  • Перевод
В 2016 году TypeScript начал брать новые высоты. Разработчики принялись полностью переписывать на него многие популярные технологии и добавлять на существующие платформы поддержку статического анализа. Такой глобальный процесс добавил больше стабильности в кодовую базу тысяч, а то и десятков тысяч проектов.

Почему React? По состоянию на сегодняшний день эта библиотека бесспорно доминирует на фоне конкурентов. Вокруг React образовалось самое большое сообщество разработчиков в мире. Каждый третий SPA написан на данной платформе. Также есть множество отличных проектов, связанных с использованием React Native, платформы для iOS, UWP и Android приложений, основанной на React.js.

Поэтому сегодня мы взглянем на возможности, которые дает интеграция двух суперпопулярных инструментов: TypeScript и React.



Примеры


Для начала разберемся, какие типы мы можем использовать для React.
Начнем с простого и добавим типы в Functional Component.

import * as React from 'react';
 
const HelloWorld: React.FunctionComponent<{
  name: string;
}> = ({ name = 'World' }) => {
  return <div>Hello, {props.name}</div>;
};
 
export default HelloWorld;

Для Functional Component или Statless Component мы должны использовать определение типа React.FunctionComponent. Так же мы можем определить типы для аргумента props — полей, которые компоненту передает родитель. В данном случае props может содержать только поле name с типом string.

Все это выглядит не сложно. А что насчет компонентов классов?

import * as React from 'react';
 
interface State {
  name: string;
}
 
interface Props {}
 
class HelloWorld extends React.Component<Props, State> {
  state = {
    name: 'World'
  }
  
  setName(name: string) {
    this.setState({ name });
  }
  
  redner() {
    return (
      <React.Fragment>
        <hI>Hello, {this.state.name}</hI>
        <input value={this.state.name} onChange={(e) => this.setName(e.target.value)} />
      </React.Fragment>
    );
  }
}

В примере с классом мы создали два интерфейса: Props и State. С их помощью мы определили сигнатуры входящих пропсов (пустые) и сигнатуру состояния компонента — как в примере с Functional Components.

Так же мы можем добавить значения пропсов по умолчанию.

import * as React from 'react';
 
interface Props {
  name?: string;
}
 
export default class HelloWorld extends React.Component<Props> {
  static defaultProps: Props = {
    name: 'World'
  };
 
  render () {
    return <hI>Hello, {this.props.name}</hI>;
  }
}

Вот и все! Наше маленькое React приложение уже строго типизировано на уровне параметров и значений состояния компонента.

Давайте разберем примущества, которые это нам дало:

  • на этапе компиляции мы увидим все несоответствия типов;
  • правильно настроенный редактор поможет нам избежать ошибок еще на этапе разработки, просто подсвечивая несхождения сигнатур или типов данных;
  • документация из интерфейсов и определений типов.


Enum в параметрах



Enum — это перечисляемый тип данных. Если мы добавим этот тип к переменной или полю интерфейса, то значением этого поля или переменной могут быть только определенные значения в Enum.
Например.

 import * as React from 'react';
 
enum Colors {
  RED,
  BLUE,
  GREEN
}
 
const ColorResult: React.FunctionComponent<{
  color: Colors;
}> = ({ color = Colors.Red }) => {
  return <div>Your color is {props.color}</div>;
};
 
export default ColorResult;

В уже знакомом нам Functional Component мы хотим показать выбранный пользователем цвет. В типе enum Colors мы указали все возможные варианты цвета, которые могут передаваться в компонент. Если компилятор TypeScript увидит где то несоответствие по типам, он покажет вам это, выдав ошибку.

Строгий Redux


В 2019 мы все еще имеем много приложений, работающих на Redux. TypeScript может помочь в данной ситуации.

import * as React from 'react';
 
const initialState = { name: 'World' };
type HelloWorldStateProps = Readonly<typeof initialState>;
 
interface Action {
	type: string;
  name?: string;
}
 
const worldNameReducer = (
	state: HelloWorldStateProps = initialState,
	action: Action
): HelloWorldStateProps => {
	switch (action.type) {
		case "SET":
			return { name: action.name };
		case "CLEAR":
			return { name: initialState.name };
		default:
			return state;
	}
};
 
const set = (name): Action => ({ type: "SET", name });
const clear = (): Action => ({ type: "CLEAR" });
 
const store = createStore(
	combineReducers({
		world: worldNameReducer
	})
);
 
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
 
interface AppProps extends StateProps, DispatchProps {}
interface AppState extends StateProps {}
 
class App extends React.Component<AppProps, AppState> {
  state = {
    name: initialState.name
  }
  
  setName(name: string) {
    this.setState({ name });
  }
 
	render() {
		const { set, clear, name } = this.props;
		return (
			<div>
				<hI>Hello, {name}</hI>
        <input value={this.state.name} onChange={(e) => this.setName(e.target.value)} />
        
        <button onClick={() => set(this.state.name)}>Save Name</button>
        <button onClick={() => clear()}>Clear</button>
			</div>
		);
	}
}
 
const mapStateToProps = ({ world }: { world: HelloWorldStateProps }) => ({
	name: world.name,
});
 
const mapDispatchToProps = { set, clear };
 
const AppContainer = connect(
	mapStateToProps,
	mapDispatchToProps
)(App);
 
render(
	<Provider store={store}>
		<AppContainer />
	</Provider>,
	document.getElementById("root")
);

В данном примере мы добавляем типы в приложение сразу на несколько уровней. В первую очередь, это сами редьюсеры. На вход редьюсер принимает Action, а возвращать он должен всегда объект соответствующий типу HelloWorldStateProps. Учитывая какое количество редьюсеров бывает в современном приложении, это очень полезное нововведение. Так же каждый action у нас имеет строгую сигнатуру Action.

Следующий уровень типизации — компонент. Здесь мы применили наследование типов для AppProps и AppState. Зачем писать больше, когда у нас уже есть типы данных с такими сигнатурами? Так и поддерживать систему проще. Если вы поменяете некоторые элементы, изменения произойдут по всем наследникам.

Заключение


TypeScript — действительно полезный язык, работающий поверх JavaScript. В связке с React он дает действительно впечатляющие практики программирования Frontend приложений.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 3

    +1
    Я прошу прощения, но кажется, что даже в простом примере не стоит писать вызов стрелочных функций в методе рендеринга. Потому что использование стрелочной функции в методе render создает новую функцию каждый раз при рендеринге компонента, что может влиять на производительность. Ну, типо это признак плохого тона что ли. За статью спасибо, легка к прочтению и усвоению
      –1

      Я, пожалуй, приведу цитату с сайта reactjs.org


      Is it OK to use arrow functions in render methods?
      Generally speaking, yes, it is OK, and it is often the easiest way to pass parameters to callback functions.
      If you do have performance issues, by all means, optimize!

      Ваше замечание, несмотря на то, что в определенных случаях имеет право на жизнь, в огромном количестве ситуаций неактуально.
      Слишком большое количество людей считают этот подход ужаснейшим антипаттерном, коим он не является

      +1
      interface Action {
      type: string;
      name?: string;
      }
      Для нормальной строгости все возможные экшены с их пайлоадом должны быть предетерминированы, а здесь сырые типы.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое