В 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 приложений.

Комментарии (2)


  1. MOZGoEZIK
    30.08.2019 10:38
    +1

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


  1. serf
    30.08.2019 15:08
    +1

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