Tinkerer

Adventures in code


React To Typescript Part 2: Converting React Components to TypeScript

This is the second in a series of blog post that will deal with converting an existing React codebase, Flatris by @Skidding to Typescript. We’ll end end up with a completely typechecked codebase. This time we’ll be looking adding type annotations to simple React components.

Where we left off last time

We can now run Javascript and Typescript side by side. That means we can convert things gradually. Let’s start by converting the React Components first - they’re easy to convert, as they already have a type-system bolted on via PropTypes. As Flatris doesn’t have a test-suite we can’t automatically test our refactorings. However, it is only one screen, so manual testing is easy. What we’ll do is we’ll rewrite one component at a time, run the game to make sure everything works.

I’ve previously written about where to start adding types to an untyped codebase, but the tl;dr is: We should start with components that have as few untyped dependencies as possible. I call this bottom up.

Now, let’s look at some different types of components to refactor.

Components that need no modification

Some components are identical between Javascript and TypeScript. These are components where the types are either provided by a third party library, or components that receives no props.

A simple example:

const Logo = () => <img src="/img/logo.png" />

Apart from the file extension, these TypeScript components are completely identical to their JS counterparts.

In Flatris, we have some of these components as well. There’s a button created with styled-components, it looks as follows:

// button.jsx
import styled from 'styled-components';

export default styled.button`
  display: block;
  margin: 0;
  padding: 0;
  border: 0;
  background: #34495f;
  color: #fff;
  font-family: Helvetica, Arial, sans-serif;
  font-weight: 300;
  outline: none;
  cursor: pointer;
  user-select: none;
`;

We’ll need the types for the library to make this work.

Some libraries ship with typescript definitions, in which case we don’t need to do any additional work. Popular libraries that don’t ship with type definitions have them under the @types namespace on npm.

As styled-components ships with its own typescript definitions, we don’t need to do anything extra - all we need to do is rename button.jsx to button.tsx and we’re good to go.

Stateless Components With Props

Adding types from the bottom up, means it makes sense to start with a component such as the SquareBlock, which is a colored, square block.


//SquareBlock.jsx

import React from 'react';
import PropTypes from 'prop-types';

import './SquareBlock.css';

/**
 * Building block for Tetrominoes and the grid of the Well, occupying a 1x1
 * square block. The only configurable property square blocks have is their
 * color.
 */
const SquareBlock = ({ color }) => (
  <div className="square-block" style={{ backgroundColor: color }} />
);

SquareBlock.propTypes = {
  color: PropTypes.string.isRequired
};

export default SquareBlock;

First thing we’ll want to do is rename it to tsx.

We’ll get the following error:

C:/…/flatris-ts/src/components/SquareBlock.tsx
(15,13): Property 'propTypes' does not exist on type '({ color }: { color: any; }) => Element'.

Typescript correctly infers the type of our function here, as as a function something that takes in a color object, and returns a JSX element. However, typescript isn’t crazy about randomly adding properties to functions. This means when we try to add the propTypes, typescript balks and tells us that this function doesn’t have a propType property - what’s up with that?

What we’ll want to do instead of simply defining the function as we did in regular Javascript above, is that we’ll want to add a type annotation to make sure Typescript considers it a proper React component - propTypes and all.

In the React typings, a functional component is called React.SFC - where SFC is Stateless Functional Component. Let’s try to add this type annotation to the component:


const SquareBlock : React.SFC = ({ color }) => (
 <div className="square-block" style={{ backgroundColor: color }} />
);

This successfully gets rid of the propTypes error - but there’s a different storm brewing..

Typing Props

Here we get a new error:

(11,36): Type '{ children?: ReactNode; }' has no property 'color' and no string index signature.’’

Here TypeScript seems to be complaining about us passing in color in our component. Weird. Let’s try to take a look at the typings for the React.SFC to see what’s going on. While this isn’t the most complex example of a type definition (for complex cases, they can get very hairy) it’s still complex enough to worth taking a closer look.

We’ll look at the type annotations one at a time, and try to figure out what’s going on. This is going to be a bit of a deeper dive into some Typescript typings - hang on.

The first typing is this:

type SFC<P = {}> = StatelessComponent<P>;

This is the SFC typing that we’re using.

This is what’s called a type alias, denoted by the ‘type’ keyword. It basically just means ‘this is a shorthand for that type’ So e.g.

type StringOrNumber = string | number

Is a type alias for the union of string and number. It is exactly equivalent to substituting StringOrNumber with string | number every time you see it.

Now, the next part of the type: The angle brackets. The part inside the angle brackets <> denote the generic type.

The generic type here is called P - for props I assume. This is the part where we tell Typescript what kind of props we’re willing to accept. It says <P = {}> - that means; ‘if no type is supplied, default to the empty object {} - meaning no additional props’

So summing up, the entire line means:

‘When you use SFC, i’ll refer to the StatelessComponent type. I’d like a generic parameter with props, but if you don’t give me one, I’m going to assume there’s no props’

So let’s have a look at the type for StatelessComponent - we’re going to look quickly at the actual code, and then take it apart.


// This is what SFC resolves to.
// Notice the same generic type P as before, that defaults to the empty // object
interface StatelessComponent<P = {}> {
   // This is the function signature. We'll explore this further below the snippet
   (props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;
   // These properties don't matter much, but worth noting here is that propTypes is marked as a property, and that is why we were able to add it earlier.
   propTypes?: ValidationMap<P>;
   contextTypes?: ValidationMap<any>;
   defaultProps?: Partial<P>;
   displayName?: string;
}

So, the first part of the interface is the interesting part, let’s take a look at it:

(props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;

What we have here is a callable signature - it means that anything conforming to this interface can be called with the following signature. Let’s try to tear it a bit apart and see what it means

The first part

(props : P & (children?: ReactNode)

is an intersection type, denoted by the &. The & means ‘combine these two types’ So this statements means: “Take in a props parameter, that is the combination of the generic parameter props, but can also have an extra prop called children.”

The ? after children denote it is optional. This allows us to pass children when we want to, via JSX. E.g.

<Foo><Bar/></Foo>

is equivalent to passing a ReactElement Bar as a children prop to Foo.

Now, the second part of the type signature:

context?: any

This means that the function can take a second parameter as well, but it is untyped. It is for access to the legacy React Context API. Now let’s have a look at the return type:

ReactElement<any> | null

This return-type means that this function either returns A React component taking whatever sort of props, or nothing

So summing up. This:

(props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;

Means this:

“A StatelessComponent can be called with Props, as specified by the generic parameter P, and optionally - children. If it takes another argument other than props, it is the context. This can be anything. The function will either return a ReactElement, or nothing.”

Whew! That was a long way around to explain our original error message. Just to jog your memory, the error message was:

(11,36): Type '{ children?: ReactNode; }' has no property 'color' and no string index signature.’’

So what TypeScript is complaining about here is the generic parameter in React.SFC. As it defaults to {}, the empty object, it means that if we don’t specify the generic part - the only thing it will accept is children - e.g. no props.

So what the error message is saying is: Hey - you told me that you aren’t receiving any props, but right afterwards you keep talking about this color variable - what’s with that?

To make sure we’re right that the generics is the issue, let’s try to say we accept props of the any type:


const SquareBlock : React.SFC<any> = ({ color }) => (
 <div className="square-block" style={{ backgroundColor: color }} />
);

This makes it compile without any problems - however TypeScript will now allow us to pass any props to SquareBlock. Now we don’t necessarily want that - the more we use the any type, the less value we get from Typescript.

We can tell from the PropTypes that the color object should be a string, so let’s capture that in a typescript interface.

These proptypes:

SquareBlock.propTypes = {
 color: PropTypes.string.isRequired
};

correspond to this interface:

interface Props {
   color: string;
}

Now that we have the compilers backing, we can actually go right ahead and remove the propTypes. While very few in TypeScript codebases keep propTypes around, when we’re still in a transitory process - you might want to keep them around for some extra run-time safety, or if you’re building something meant to be consumed by non-typescript usage, e.g. libraries.

The final component without proptypes looks like this


import React from 'react';

import './SquareBlock.css';

interface Props {
   color: string;
}

/**
* Building block for Tetrominoes and the grid of the Well, occupying a 1x1
* square block. The only configurable property square blocks have is their
* color.
*/
const SquareBlock : React.SFC<Props> = ({ color }) => (
 <div className="square-block" style={{ backgroundColor: color }} />
);

export default SquareBlock;

Class based component

Lets take a little more complicated component - the Tetrimono. A Tetromino is a collection of four square blocks. They’re what most people would call a “Tetris piece” I think.

I didn't even know these had a name

//tetromino.jsx
import React from 'react';
import PropTypes from 'prop-types';
import SquareBlock from './SquareBlock';

import './Tetromino.css';

class Tetromino extends React.Component {
 /**
  * A Tetromino is a geometric shape composed of four squares, connected
  * orthogonally. Read more at http://en.wikipedia.org/wiki/Tetromino
  */
 renderGridBlocks() {
   const blocks = [];
   const rows = this.props.grid.length;
   const cols = this.props.grid[0].length;

   for (let row = 0; row < rows; row++) {
     for (let col = 0; col < cols; col++) {
       if (this.props.grid[row][col]) {
         blocks.push(
           <li
             className="grid-square-block"
             key={`${row}-${col}`}
             style={{
               top: `${row * 25}%`,
               left: `${col * 25}%`
             }}
           >
             <SquareBlock color={this.props.color} />
           </li>
         );
       }
     }
   }

   return blocks;
 }

 render() {
   return <ul className="tetromino">{this.renderGridBlocks()}</ul>;
 }
}

Tetromino.propTypes = {
 color: PropTypes.string.isRequired,
 grid: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)).isRequired
};

export default Tetromino;


The Tetromino is a class based component here, so we’ll have to do things a little differently. Here we don’t need to add any explicit type. When extend React.Component - TypeScript picks up the typings automatically.

If we look at the class definition for React.Component it looks something like this:

// Note that this is not the actual typings - the react typings are reasonably complex, so I've edited them a bit down to communicate the gist of it
class Component<P={}, S={}> {
    constructor(props: P, context?: any);
    // ...Stuff removed for brevity
}

Note the two generic parameters that default to the empty object. While we can write code like this:

class Tetromino extends React.Component {}

and it’ll be totally valid Typescript code. However as with the functional component, it’s going to give us errors if we want to either use state or props. We’ll need to define the generic types to get proper type-safety.

As a first-shot at getting it to compile, we can of course specify them as any:

class Tetromino extends React.Component<any, any> {}

However - we can do better. Looking at the Tetromino, we can see it contains no state - so let’s change the second parameter to void:

class Tetromino extends React.Component<any, void> {}

Or even better - we can just leave it out!

class Tetromino extends React.Component<any> {}

Now let’s tackle the props! We’ll rewrite the propTypes into an interface again. The propTypes look like this:

Tetromino.propTypes = {
 color: PropTypes.string.isRequired,
 grid: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)).isRequired
};

So we have a string, and a array of arrays of numbers. This can be expressed in typescript like this:

interface Props {
   color : string,
   grid: number[][] // Note the double square brackets - that's a twice nested array
}

And we add that to the React.Component, so our new class definition is now :

class Tetromino extends React.Component<Props> {

Now - we’re not quite done yet. We’re going to get another propTypes error:

Property 'propTypes' does not exist on type 'typeof Tetromino'.

So, Typescript isn’t crazy about us declaring new properties on already existing classes - the best way to declare the propTypes as a class based property is with a static block. So instead of :

Tetromino.propTypes = {
 color: PropTypes.string.isRequired,
 grid: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)).isRequired
};

we get:

class Tetromino extends React.Component<Props> {
   static propTypes = {
       color: PropTypes.string.isRequired,
       grid: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)).isRequired
   };
   //..Other stuff here
}

Of course we can simply omit the propTypes altogether. In full we’ll end up with the following class.


interface Props {
   color: string,
   grid: number[][]
}

class Tetromino extends React.Component<Props> {
   /**
    * A Tetromino is a geometric shape composed of four squares, connected
    * orthogonally. Read more at http://en.wikipedia.org/wiki/Tetromino
    */
   renderGridBlocks() {
       const blocks = [];
       const rows = this.props.grid.length;
       const cols = this.props.grid[0].length;

       for (let row = 0; row < rows; row++) {
           for (let col = 0; col < cols; col++) {
               if (this.props.grid[row][col]) {
                   blocks.push(
                       <li
                           className="grid-square-block"
                           key={`${row}-${col}`}
                           style={{
                               top: `${row * 25}%`,
                               left: `${col * 25}%`
                           }}
                       >
                           <SquareBlock color={this.props.color}/>
                       </li>
                   );
               }
           }
       }

       return blocks;
   }

   render() {
       return <ul className="tetromino">{this.renderGridBlocks()}</ul>;
   }
}

export default Tetromino;

Looks pretty much the same right? If you’re already specifying types via propTypes, you can often simply replace the propTypes with typescript interfaces. Often you won’t need to add any type annotations to the meat of your components at all - typescript can infer it for you.

In the next part, we’ll look at using simple run-time analysis to type components we can’t quite figure out how to type by simply looking at propTypes. We’ll also look at typing reducers and actions. If you have any comments or questions, reach out to me at @GeeWengel.

If you just want to be notified when the next part is live - subscribe to my newsletter. I email very rarely, as I’m very lazy.