The release of TypeSciprt 1.6 includes support for React components. I decided to give it a whirl and see what TypeScript has to offer.

The end result is a port of the Redux TodoMVC example to TypeScript. (See my repo on GitHub)

So why use types in the first place? Isn’t the dynamic and flexible nature of JavaScript what makes it so great in the first place? As strange as it sounds, I actually believe that adding more constraints to the system can result in more freedom. Of course, the type system has to be powerful and flexible enough to not get in your way.

This post is meant for developers who work with JavaScript and has an interest in TypeScript and React. I will be showing examples using the Redux framework, but it is not a requirement. No prior knowledge of TypeScript is required, but you should have some familiarity with React.

I will cover the following topics:

  • What are types, and why we need them?
  • How TypeScript can help us develop React applications.
  • Additional considerations when choosing TypeScript and React.
Update (2017-10-12): I've updated the code to use TypeScript 2.5.3, ReactJS 16.0.0, and Redux 3.7.2. It now uses typings as opposed to tsd, which is the direction the community has adopted.

Types are good for you

If you have experience in Java (or in a similar statically typed language), you may be immediately turned off by types. The first thing to note is that a type in TypeScript is not a class. A more mathematical definition of a type is that a type is a name given to the set of inputs and outputs of a given function. That is, a type describes the interface of a function. This is the way I think about types.

Say I have a function defined as follows:

const add = (a: number, b: number): number => a + b;

The add function above says that it takes in two parameters a and b both of type number, then outputs a number. Thus, if I write any of the following statements, I will get a compile error.

// '2' is not a number.
add(1, '2');

// result declared to be string, but add returns a number.
const result: string = add(1, 2);

If you are using an IDE that supports TypeScript you will get editor hints as you are typing.

As seen above, types gives us two benefits.

  1. Catch errors early on during compile time, not runtime.
  2. Serves as documentation for functions. And in editors that support TypeScript, we get type hints as we code.

Expanding on point #2, the type hints are especially useful for options object, as seen in libraries like jQuery.

Say I have this function defined below.

// The | here denotes a union type, which will be covered later in this post.
// options? means the second parameter is not required.
const doSomething = (x: number | string | CustomClass, options?: Options): void => {
  // ...
};

As a user of doSomething I might wonder what options I am allowed to pass in. With the Options type defined below, I will get hints in our editor as I code.

interface Options {
  flag1?: boolean;
  flag2?: boolean;
  callback?: (c: CustomClass) => void;
}

More on types

Before we move on to React examples, I want to expand on types a bit more.

As mentioned earlier, types in TypeScript are not classes. The following defines a Todo type, but does not create a class.

interface Todo {
  id?: number;
  text: string;
  completed: boolean;
}

The id? syntax means that the id property is optional (since a new Todo may not have an ID yet).

Given the above definition, I can now do the following.

// Valid assignment.
const t1: Todo = {
  id: 1,
  text: 'Finish this blog post',
  completed: false
};

// Type error because text is missing.
const t2: Todo = {
  id: 2,
  completed: false
};

What is more interesting is that I can use the type to form an interface over a function that operate over Todos.

const completedTodos = (todos: Todo[]): Todo[] => {
  return todos.filter(t => t.completed);
};

// This works.
completedTodos([
  { id: 1, text: 'Do something', completed: true },
  { id: 2, text: 'Do another thing', completed: false }
]);

// These fail because inputs are the wrong type.
completedTodos('Huh?');
completedTodos([{ text: 'Missing completed property' }]);

Again, you will see errors in editors that support TypeScript as you are typing.

Function types

You can even define types for functions.

interface NumberToString {
  (x: number): string;
}

const applyNumberToString = (f: NumberToString, x: number): string => {
  return f(x);
};

Union types and type guards

The last thing I will show is union types. A variable of an union type can be assign any type within that union.

type numberOrString = number | string;

let x: numberOrString;

x = 1;    // OK
x = '1';  // OK
x = true; // BAD

Notice that there are no interfaces or subclasses defined above for numberOrString. Union types can be combined with type guards to help manage branches within a function.

const doubleIfNumber = (x: numberOrString): numberOrString => {
  if (typeof x === 'number') {
    return x * 2; // This is OK because we know x is a number!
  } else {
    return x;
  }
}

doubleIfNumber(2);   // 4
doubleIfNumber('2'); // '2'

The above features are enough for us to jump into the TodoMVC application. If you want to learn more about TypeScript, I found the Handbook to be a useful resource.

TodoMVC in React, Redux, and TypeScript

By now you have seen some of the benefits of using a powerful type system. But how does this fit in with React?

With TypeScript 1.6, you can now write your React components in TSX files. The following is an example from my TodoMVC example.

// `The * as ____` here is a way to import ES6 default exports in ES6.
import * as React from 'react';

interface TodoTextInputProps {
  onSave: Function;
  text?: string;
  placeholder?: string,
  editing?: boolean;
  newTodo?: boolean;
}

// This component's props have to match TodoTextInputProps inferface.
// We can do the same for the component's state. In this example, it is set to any.
class TodoTextInput extends React.Component<TodoTextInputProps, any> {
  render() {
    // ...
  }
}

Note: Since React does not provide TypeScript definitions, we cannot use it without providing ambient module definitions (e.g. .d.ts files). In this case, I am using a tool called typings to install the missing definitions.

You can view the typings.json file to see all of the definitions I have installed.

Properties interface vs React.PropTypes

Properties interface differs from React.PropTypes in that the latter will only give you errors during runtime. With Properties interface we can get feedback immediately from the compiler.

// This errors on compile because onSave is required but not specified.
<TodoTextInput/>

// This errors because onSave is the wrong type.
<TodoTextInput onSave={true}/>

// This errors because onEdit is not a valid property.
<TodoTextInput onEdit={() => {}} onSave={() => {}}/>

Unfortunately, as of this writing Visual Studio Code (the editor I am using) does not seem to support TSX files yet. The above errors were from Webpack with ts-loader.

Types and Redux

In the TodoMVC example, I defined a Todo type that is used throughout the application. It is used as the return type of actions/todos . The state of reducers/todos is Todo[]. And the components all take in Todo or Todo[] as properties.

This ensures that as I am coding, my actions and reducers (stores) all work with the correct types.

/* actions/todos.ts */

// This action takes in a `string` and returns `Todo` as its payload.
const addTodo = createAction<Todo>(
  types.ADD_TODO,
  (text: string) => ({ text, completed: false })
);

/* reducers/todos.ts */

// This reducer takes current state of Todo[] and returns a new state of Todo[].
export default handleActions<Todo[]>({
  [ADD_TODO]: (state: Todo[], action: Action): Todo[] => {
    return [{
      id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
      completed: action.payload.completed,
      text: action.payload.text
    }, ...state];
  }
  // ...
}, initialState);

I now benefit from all the type hints and errors when working with my actions and reducers.

Using union types as models

Let’s add an initializing state to the todos reducer by introducing a Model union type for it.

// This denotes that the state is still initializing.
// There is a progress property that is a number from 0-100 (%).
export interface Initializing {
  progress: number;
};

// The model of our state can be either of these types.
export type Model = Initializing | Todo[];

// Check if x is a type of Initializing. This can be used in type guards.
export const isInitializing = (x: any): x is Initializing => {
  return typeof x.progress === 'number';
}

We’ll have to change our reducer’s state from type Todo[] to Model, and add type guards to our reduce functions.

export default handleActions<Model>({
  [ADD_TODO]: (state: Model, action: Action): Model => {
    let todos: Todo[];

    // Type guard for state.
    if (isInitializing(state)) {
      // If current state is initializing, set todos to empty array.
      todos = [];
    } else {
      // Otherwise set to current state.
      todos = state;
    }

    return [{
      id: todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
      completed: action.payload.completed,
      text: action.payload.text
    }, ...todos];
  }
  // ...
});

The type guard ensures that in the else branch, state is of type Todo[], which is why the above code compiles.

Note: I chose to not use a class for Initializing, but used an interface instead. The downside of this approach is that you cannot use state instanceof Initializing in the type guard since interfaces are not available for reflection during runtime. It's up to you how you want to implement your own types.

Rendering the initializing state

Within my App component, I can use the same Model and type check function to render an initializing state.

render() {
  const todos: Model = this.props.todos;
  const dispatch = this.props.dispatch;
  const actions = bindActionCreators(TodoActions, dispatch);

  if (isInitializing(todos)) {
    const style = {
      'font-size': '24px',
      'text-align': 'center'
    };
    return <p style={style}>Initializing... ({todos.progress}%)</p>
  } else {
    return (
      <div className="todoapp">
        <Header addTodo={actions.addTodo} />
        <MainSection
          todos={todos}
          actions={actions}/>
      </div>
    );
  }
}

The alternative to this approach might be to change our state to the following type.

// todos can now be undefined or null.
interface Model {
  todos?: Todo[];
  isInitializing: boolean;
  initializationProgress: number;
}

This trades in the union type for a boolean flag to let us know why todos is undefined or null. This makes our state much harder to reason about when we add in more and more flags and metadata. From my experience with real-world applications, doing this type of thing is hard to avoid without types.

This initialization example is a bit contrived, but I hope it shows the power behind the technique. I have a branch of that shows how the above code works in the TodoMVC app. Fair warning, it is not completely functional because I only did enough work to get a few examples.

Closing Remarks

In this post I offered a glimpse of how TypeScript can help you when writing a React application. Some benefits you will receive are:

  • Type hints as you code (in editors that support TypeScript).
  • Type errors during compile time, or as you code in supported editors.
  • Guarantees against certain classes of errors when your application compiles successfully. (typos, wrong props usage, etc.)
  • Union types to simplify application state.

Does this mean you should rewrite your React application in TypeScript right now? It’s up to you. There are downsides in choosing TypeScript.

  • Your favourite editor may not support TypeScript. Best editors right now would be WebStorm or Visual Studio Code IMO.
  • As of this writing, Visual Studio Code does not support TSX files (at least from my observation).

    (As Franck pointed out in the comments, you can point the typescript.tsdk option in your VSC settings.json file to the lib directory of your TypeScript install)

  • You will need to invest in more tools (TSD, ts-loader for Webpack, etc.).

If you think the benefits outweigh the the costs, definitely give TypeScript a go!

Resources



blog comments powered by Disqus