Static typing in a React application

In 2016, TypeScript began to take on new heights. The developers began to completely rewrite many popular technologies on it and add support for static analysis to existing platforms. Such a global process added more stability to the code base of thousands, and even tens of thousands of projects.



Why React As of today, this library undoubtedly dominates against the background of competitors. Around React, the largest community of developers in the world has formed. Every third SPA is written on this platform. There are also many great projects related to using React Native, a platform for iOS, UWP and Android applications based on React.js.



Therefore, today we will take a look at the possibilities that the integration of two super popular tools gives: TypeScript and React.







Examples



First, let's see what types we can use for React.

Let's start simple and add types to Functional Component.



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





For Functional Component or Statless Component, we must use a definition of type React.FunctionComponent. We can also define types for the props argument - fields that the parent passes to the component. In this case, props can only contain a name field of type string.



All this does not look complicated. What about class components?



 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> ); } }
      
      





In the class example, we created two interfaces: Props and State. With their help, we determined the signatures of incoming props (empty) and the signature of the state of the component - as in the example with Functional Components.



We can also add default props values.



 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>; } }
      
      





That's all! Our little React application is already strongly typed at the level of parameters and state values ​​of the component.



Let's look at the advantages that this gave us:







Enum in parameters





Enum is an enumerated data type. If we add this type to a variable or interface field, then the value of this field or variable can only be specific values ​​in Enum.

For example.



  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;
      
      





In the Functional Component we already know, we want to show the color selected by the user. In the enum Colors type, we specified all possible color options that can be transmitted to the component. If the TypeScript compiler sees a type mismatch somewhere, it will show you this by throwing an error.



Strict Redux



In 2019, we still have many applications running on Redux. TypeScript can help in this situation.



 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") );
      
      





In this example, we add types to the application at once on several levels. First of all, it is the reducers themselves. The input reducer accepts Action, and it must always return an object corresponding to the type HelloWorldStateProps. Given how many reducers there are in a modern application, this is a very useful innovation. Also, every action we have has a strict Action signature.



The next level of typing is component. Here we have applied type inheritance to AppProps and AppState. Why write more when we already have data types with such signatures? It’s easier to maintain the system. If you change some elements, the changes will occur for all heirs.



Conclusion



TypeScript is a really useful language running on top of JavaScript. In conjunction with React, it provides truly impressive programming practices for Frontend applications.



All Articles