DEV Community

DmitryKotlyarenko
DmitryKotlyarenko

Posted on

Static Typing in a React Application

In 2016, TypeScript began to regain popularity. Developers have completely rewritten many popular technologies using this programming language and added static analysis tools to existing platforms. Using such a global process adds stability to the code base of thousands or even tens of thousands of projects.

Why did I choose React for app development? Today, this library undoubtedly dominates its competitors. The world’s largest developer community has formed around React. Every third SPA is written on this platform. Also, there are many great projects involving the use of React Native, a platform for iOS, UWP, and Android app development based on React.js.

The integration of two super-popular tools, TypeScript and React, offers great opportunities for app developers.

Examples

First, let's clarify what types we can use with React.
We will start with the basics and then add types to the 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 the Functional Component (or Stateless Component), we must use a definition of the “React.FunctionComponent” type. We can also define types for the “Props” argument, i.e. fields that the parent passes to the component. In this case, “Props” can only contain a "name" field with the "string" type.

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>
        <h1>Hello, {this.state.name}</h1>
        <input value={this.state.name} onChange={(e) => this.setName(e.target.value)} />
      </React.Fragment>
    );
  }
}

In the example with "class", we created two interfaces: Props and State. With their help, we determined the signatures of incoming props (empty) and the signature of the component's state, 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 <h1>Hello, {this.props.name}</h1>;
  }
}

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

Let's consider the advantages:

  • we can see all type mismatches at the compilation stage;
  • a properly configured editor will help us avoid mistakes (even at the development stage) by simply highlighting the discrepancies between signatures or data types;
  • we have documentation on interfaces and type definitions.

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 will be equal to 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 already familiar Functional Component, we can show the color selected by the user. In the "enum Colors" type, we should specify all possible color options that can be transmitted to the component. If the TypeScript compiler finds a type mismatch, it will display an error alert.

Strict Redux

Today, we still have many applications running on Redux. TypeScript can improve them.

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>
                <h1>Hello, {name}</h1>
        <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, I added types to several app levels at once. First of all, I worked with reducers. A reducer accepts "Action" on input and should return objects corresponding to the "HelloWorldStateProps" type. Given the great number of reducers in an average application, this is a very useful innovation. Every action has a strict "Action" signature.

The next level of typing is a component. I have applied type inheritance to AppProps and AppState. Why should I write more if data types with such signatures are already available? With this approach, it is much easier to maintain the system. If some elements are changed, all the heirs will change as well.

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.

Top comments (0)