How to Add TypeScript to an Existing React Native Application

Author

Spencer Carli

Developer, cat dad, and devout pizza lover. Teaching at React Native School and building apps with Handlebar Labs.

Last Updated: January 6, 2021

Do you want to add TypeScript to your React Native app (if not we've covered why you should use TypeScript in a React Native App) but don't know how you would possibly migrate the entire app over at once? I've felt the same!

Fortunately, we can do so incrementally.

Note: If you're simply starting a new React Native project and are ready to use TypeScript you can do so by running npx react-native init MyApp --template react-native-template-typescript. This template is what we'll be following along with in this tutorial.

Another quick note, I use Visual Studio Code as my editor. The TypeScript integration is automatic and great. You'll want to investigate how to set up TypeScript in your editor to get the full value.

Installation

We'll be migrating the code from our class showing you how to test React Native apps. If you'd like to follow along this code exists on Github.

First, we need to install the various packages.

Terminal

yarn add --dev typescript

Then we need to add types for all the packages we use.

Terminal

yarn add --dev @types/jest @types/jest @types/react @types/react-native @types/react-test-renderer

Next, at the root of your project, create a tsconfig.json file and paste the following. This is pulled from the typescript template I mentioned above.

tsconfig.json

{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "jsx": "react-native",
    "lib": ["es2017"],
    "moduleResolution": "node",
    "noEmit": true,
    "strict": true,
    "target": "esnext"
  },
  "exclude": [
    "node_modules",
    "babel.config.js",
    "metro.config.js",
    "jest.config.js"
  ]
}

At this point you shouldn't notice anything different in your project.

Migrating JS to TypeScript in React Native

Let's start the migration! We'll start at App/util/api.js, which looks like this:

App/util/api.js

export const api = (path, options = {}) => {
  return fetch(`https://jsonplaceholder.typicode.com${path}`, options)
    .then((res) => res.json())
    .then((response) => {
      if (!Array.isArray(response) && Object.keys(response).length === 0) {
        throw new Error('Empty Response');
      }

      return response;
    });
};

To migrate to TypeScript change the .js to .ts. You'll notice that everything still works because valid JS is valid TS.

TypeScript can implicitly set types for you, as you can see it's doing for me.

Implicit Types

You can also see that I've got a warning showing up. It doesn't like that I don't have a type specified for the path. We know that it's a string so let's say that.

App/util/api.ts

export const api = (path: string, options = {}) => {
  return fetch(`https://jsonplaceholder.typicode.com${path}`, options)
    .then((res) => res.json())
    .then((response) => {
      if (!Array.isArray(response) && Object.keys(response).length === 0) {
        throw new Error('Empty Response');
      }

      return response;
    });
};

We can even take it a step further and specify the return type. TypeScript already knows we'll return a Promise but the actual content of the promise response is specified as any. We know it will either be an object, array, or error, so we can specify that.

App/util/api.ts

export const api = (
  path: string,
  options = {}
): Promise<object | Array<object> | Error> => {
  return fetch(`https://jsonplaceholder.typicode.com${path}`, options)
    .then((res) => res.json())
    .then((response) => {
      if (!Array.isArray(response) && Object.keys(response).length === 0) {
        throw new Error('Empty Response');
      }

      return response;
    });
};

Migrating JSX to TypeScript in React Native

Next let's look at App/screens/Post.js.

App/screens/Post.js

import React from 'react';
import {
  SafeAreaView,
  Text,
  TouchableOpacity,
  StyleSheet,
  FlatList,
  View,
  ScrollView,
} from 'react-native';
import { api } from '../util/api';

const styles = StyleSheet.create({
  content: {
    paddingHorizontal: 10,
  },
  title: {
    fontWeight: 'bold',
    marginTop: 20,
  },
});

class PostList extends React.Component {
  state = {
    post: {},
    comments: [],
  };

  componentDidMount() {
    const postId = this.props.navigation.getParam('postId');
    this.getPost(postId);
    this.getComments(postId);
  }

  getPost = (postId) => {
    api(`/posts/${postId}`).then((post) => {
      this.setState({ post });
    });
  };

  getComments = (postId) => {
    api(`/posts/${postId}/comments`).then((comments) => {
      this.setState({ comments });
    });
  };

  render() {
    return (
      <SafeAreaView>
        <ScrollView contentContainerStyle={styles.content}>
          <Text style={styles.title} testID="post-title">
            {this.state.post.title}
          </Text>
          <Text>{this.state.post.body}</Text>
          <Text style={styles.title}>Comments</Text>
          <FlatList
            data={this.state.comments}
            renderItem={({ item }) => (
              <View>
                <Text>{item.name}</Text>
              </View>
            )}
            keyExtractor={(item) => item.id.toString()}
          />
        </ScrollView>
      </SafeAreaView>
    );
  }
}

export default PostList;

First step is to convert it to Post.tsx. In TypeScript it is important to add the x to the end of the file specify that it is JSX based.

Pro tip: There is no harm in naming a non-JSX file .tsx. I often use .tsx for every file I write in TypeScript so that if I later use JSX in that file I don't have to change the file type.

When we do that we see quite a few errors pop up.

Post.tsx TypeScript Errors

Let's do the easy ones first - postId.

App/screens/Post.tsx

// ...

class Post extends React.Component {
  state = {
    post: {},
    comments: [],
  };

  componentDidMount() {
    const postId = this.props.navigation.getParam('postId');
    this.getPost(postId);
    this.getComments(postId);
  }

  getPost = (postId: number) => {
    api(`/posts/${postId}`).then((post) => {
      this.setState({ post });
    });
  };

  getComments = (postId: number) => {
    api(`/posts/${postId}/comments`).then((comments) => {
      this.setState({ comments });
    });
  };

  // ...
}

export default Post;

Next we'll do the component props. Since props is an object I'm going to create an interface (that we'll also export in case we need to access it elsewhere).

App/screens/Post.tsx

// ...
export interface PostProps {
  navigation: any;
}

class Post extends React.Component<PostProps> {
  state = {
    post: {},
    comments: [],
  };

  // ...
}

export default Post;

Challenge: Using @types/react-navigation use the proper types for navigation rather than any.

Next we'll do component state. Similar process to props...

App/screens/Post.tsx

// ...
export interface PostState {
  post: PostType;
  comments: Array<CommentType>;
}

class Post extends React.Component<PostProps, PostState> {
  state = {
    post: {},
    comments: [],
  };

  // ...
}

export default Post;

Where are these PostType and CommentType coming from? We need to define them. If they're something used in many places throughout your app then you should define them in some centralized location. We'll just do it in this file for simplicity.

App/screens/Post.tsx

// ...
export interface PostType {
  title?: string;
  body?: string;
  id?: number;
}

export interface CommentType {
  name: string;
  id: number;
}

export interface PostState {
  post: PostType;
  comments: Array<CommentType>;
}

class Post extends React.Component<PostProps, PostState> {
  state: PostState = {
    post: {},
    comments: [],
  };

  // ...
}

export default Post;

It specified what should be there and what type it should be.

The finished file:

App/screens/Post.tsx

import React from 'react';
import {
  SafeAreaView,
  Text,
  TouchableOpacity,
  StyleSheet,
  FlatList,
  View,
  ScrollView,
} from 'react-native';
import { api } from '../util/api';

const styles = StyleSheet.create({
  content: {
    paddingHorizontal: 10,
  },
  title: {
    fontWeight: 'bold',
    marginTop: 20,
  },
});

export interface PostType {
  title?: string;
  body?: string;
  id?: number;
}

export interface CommentType {
  name: string;
  id: number;
}

export interface PostState {
  post: PostType;
  comments: Array<CommentType>;
}

export interface PostProps {
  navigation: any;
}

class PostList extends React.Component<PostProps, PostState> {
  state: PostState = {
    post: {},
    comments: [],
  };

  componentDidMount() {
    const postId = this.props.navigation.getParam('postId');
    this.getPost(postId);
    this.getComments(postId);
  }

  getPost = (postId: number) => {
    api(`/posts/${postId}`).then((post: PostType) => {
      this.setState({ post });
    });
  };

  getComments = (postId: number) => {
    api(`/posts/${postId}/comments`).then((comments: Array<CommentType>) => {
      this.setState({ comments });
    });
  };

  render() {
    return (
      <SafeAreaView>
        <ScrollView contentContainerStyle={styles.content}>
          <Text style={styles.title} testID="post-title">
            {this.state.post.title}
          </Text>
          <Text>{this.state.post.body}</Text>
          <Text style={styles.title}>Comments</Text>
          <FlatList
            data={this.state.comments}
            renderItem={({ item }) => (
              <View>
                <Text>{item.name}</Text>
              </View>
            )}
            keyExtractor={(item) => item.id.toString()}
          />
        </ScrollView>
      </SafeAreaView>
    );
  }
}

export default PostList;

Configuring Tests

To get your tests working with TypeScript first you need to tell Jest to look for TypeScript files by adding moduleFileExtensions to the jest object in your package.json. We'll also change the setup file to .ts.

package.json

{
  // ...
  "jest": {
    "preset": "react-native",
    "automock": false,
    "setupFiles": ["./setupJest.ts"],
    "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
  }
  // ...
}

Next, we'll convert setupJest.js to setupJest.ts and inform our tests of the mocking methods available.

setupJest.ts

import { GlobalWithFetchMock } from 'jest-fetch-mock';

const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;
customGlobal.fetch = require('jest-fetch-mock');
customGlobal.fetchMock = customGlobal.fetch;

Now, in all of our tests, we need to convert fetch to fetchMock to alleviate any TypeScript errors in a test that exists in a TypeScript file (such as in App/util/__tests__/api.test.ts).

Wrapping Up

Now you can incrementally start adopting TypeScript in your React Native app. It's no small task but adding static type checking can help you identify typos that lead to bugs before your tests ever have to run.

You can see the entire migration process for the example app in this commit.

Additional Resources

React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our classes and our private Slack community.