Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript
Tags: typescript, reactnative, eslint, prettier
Published: 2020-07-20 11:04:21

---
title: Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript
published: true
date: 2020-07-20 10:40:09 UTC
tags: TypeScript,reactnative,eslint,prettier
canonical_url: https://justinnoel.dev/2020/07/20/enforcing-consistent-and-error-free-code-in-an-expo-react-native-project-with-typescript/
cover_image: https://justinnoel.dev/content/images/2020/07/eslint-twitter-image-2.png
---
Have you ever done any development in a team environment - anywhere it's not just one person committing code? You've probably seen inconsistent code formating, different structure, etc. You've probably even been in a few "tabs vs. spaces" debates - perhaps more than a few times on the same team 🤦‍♂️.

Several teams I've worked with have solved these problems by either using [EditorConfig](https://editorconfig.org) or [Prettier](https://prettier.io) in their IDEs. However, this has alway lead to someone's IDE or config file being slightly different and not always being consistent.

Another pain point in group development is with code that has cruft. Say for example, someone adds a variable or imports a library in a file and never actually uses them. Other teams might have a "rule" of "No `for` loops!", but they have no way to enforce it other than code reviews. We know many of our team's rules are going to slip through the cracks.

The best way to solve all these problems, improve productivity, and prevent constant bike shedding is to have automated tools enforce code quality. Don't worry, this doesn't mean you can't choose your own rules, it's just that you won't have to repeatedly insist on them during code reviews. After implementing the suggestions in this post, your code will be "perfect" - but not necessarily bug free (that's on you).

**TL;DR:** You can see the final configuration on this Git repo: https://github.com/calendee/expo-lint-demo

![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-0-perfect.gif)

> The open source tools I'll present here will save you & your team/company countless hours of bickering or developing custom tooling.

> Please consider contributing to them to ensure the developers have the funding and motivation necessary to keep making these great tools.

> You can find links to several of these tools on my own contribution record on [Open Collective](https://opencollective.com/justinnoel) or their respective GitHub Sponsorship pages.

In this article, I'm going to walk through starting a brand new [Expo](https://expo.io) tabs project (this whole article can be used for **ANY** React or React Native project) from scratch and do the following:

- Configure Prettier
- Configure Linting with TypeScript
- Solve all the existing linting & formatting issues
- Setup auto-commit hooks that enforce all the new configuration rules
- Teach you how to speed up your auto-commit hooks
- Explain the neeed for CI/CD build tools to prevent anyone from bypassing the team's rules before PRs are merged

## Start an Expo Project

Our base project is going to be the default Expo "tabs" project with TypeScript.

```
expo init expo-lint
```

The Expo CLI will prompt us to choose a template and then install it for us.

![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-1.png)

![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-2.png)

> NOTE: The configuration ideas from this point forward are from several of my own older blog posts and code I liberally copied from [Khalil Stemmler's great series](https://khalilstemmler.com) on doing similar things in TypeScript projects.

## Configure ESLint for TypeScript

First, we need to get [ESLint](https://eslint.org) configured to support TypeScript:

```
yarn add --dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
```

Now, let's add an `.eslintrc` file in our project's root directory so that it looks like this:

```
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended"
  ]
}
```

Next, we'll need to add an `.eslintignore` file to ensure our linter doesn't go to crazy and start linting every directory in our project:

```
node_modules
dist
.expo
.expo-shared
web-build
```

Now, we'll need to add a lint script to our `package.json`:

```
{
...,
"lint": "eslint . --ext .ts,.tsx,.js,.jsx,.json",
"lint-and-fix": "eslint . --ext .ts,.tsx,.js,.jsx,.json --fix",
...
}
```

The two script above will let you run `yarn lint` or `yarn lint-and-fix` to see/fix all the linting errors in your project.

If you run `yarn lint` right now, you'll see an explosion of errors/warnings 🤯!

![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-3.png)


Let's just ignore those for now and move along.

## Installing and Configuring Prettier

To install [Prettier](https://prettier.io), run this command

```
yarn add --dev prettier
```

Now, we need to add the magic sauce to keep all your files formatted just the way your team likes (after arguing for a few days).

Add a `.prettierrc` file to look something like this:

```
{
  "semi": true,
  "trailingComma": "all",
  "singleQuote": false,
  "printWidth": 80
}

```

Now, everytime you save a file, your IDE will hopefully respect this file and format it just the way you've requested. You may need to adjust its settings to get this to work properly.

For example, Prettier will automatically convert `const a = '1'` to `const a = "1";` every time you save the file.

However, the real magic will happen later on. Even if someone codes in VI with no IDE assistance, we'll make sure those files are formatted properly.

We also need to keep Prettier and ESLint from whacking each other over the head. Modify your `.eslintrc` file to look like this:

```
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": ["react", "@typescript-eslint", "prettier"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ]
}
```

Finally, add a script to `package.json` to run prettier on all your existing files:

```
{
...,
"prettier-format": "prettier --config .prettierrc '**/*.{json,js,jsx,ts,tsx,css,scss,md}' --write",
...
}
```

This script will automatically cleanup all the files we care about. If other file types are needed, include them inside the brackets with a comma - be sure to NOT have any spaces!

Now, run `yarn prettier-format` and watch it automagically format many of the files in our project. Just like that, we've updated the formatting for every file in the project. Now, if a team member modifies one of those files days or weeks later, their PR won't be polluted with tons of formatting changes that hide their real changes.


![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-4.png)

## Configuring React & Jest Linting Support

When we ran `yarn lint` earlier, there was something pretty strange in the output. It frequently pointed out things like:

- `warning 'React' is defined but never used @typescript-eslint/no-unused-vars` 
- `warning 'View' is defined but never used @typescript-eslint/no-unused-vars`
- `warning 'TouchableOpacity' is defined but never used @typescript-eslint/no-unused-vars`
- `warning 'it' is not defined`
- `warning 'expect' is not defined`

WTH??? The problem is that ESLint doesn't understand React and JSX. So, we need to introduce it to them.

![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-5.gif)

```
yarn add --dev @types/react @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-config-react eslint-plugin-prettier eslint-plugin-jest eslint-plugin-react
```

Now, once again, edit the `.eslintrc` file as follows:

```
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": ["react", "@typescript-eslint", "prettier", "jest"],
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier",
    "plugin:jest/recommended"
  ],
  "rules": {
    "no-console": 1,
    "prettier/prettier": 2,
    "jest/no-disabled-tests": "warn",
    "jest/no-focused-tests": "error",
    "jest/no-identical-title": "error",
    "jest/prefer-to-have-length": "warn",
    "jest/valid-expect": "error"
  },
  "settings": {
    "react": {
      "pragma": "React",
      "version": "detect"
    }
  },
  "env": {
    "jest/globals": true
  }
}
```

If we run `yarn lint` again, there are still a ton of problems, but `React` ain't one of em.

## Adding pre-commit Hooks with Husky

We've finally got our project configured to lint and prettify everything! Now, let's configure [Husky](https://github.com/typicode/husky) to make sure our code can't be committed with linting issues and that all code is automatically prettified.


```
yarn add --dev husky
```


Now, modify the `scripts` section of `package.json` again:


```
{
...
"husky": {
  "hooks": {
    "pre-commit": "yarn prettier-format && yarn lint"
  }
},
...
}
```


Now, every time a developer runs `git commit -m"here's my awesome code!"` Husky will try to run prettier and linting to make sure that code really is "awesome".

Running that right now, results in a big, " **NOT!**"

Husky blocks the commit because there are a ton of errors in the code. Woohoo! Mission accomplished!



![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-6-mission-accomplished.gif)


## Fixing All the Linting Errors

Our new `yarn lint` output frequently shows issues like this:


```
/Users/jn/Documents/Apps/expo-lint/App.tsx
  10:16 warning Missing return type on function @typescript-eslint/explicit-module-boundary-types
```

### Missing Return Type

In our IDE, the `function App()` is underlined to show that ESLint thinks something is wrong with this code.



![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-7.png)



Moving the mouse over the underlined code will show a hint about what is wrong:



![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-8.png)



The issue is that TypeScript really likes to have the return value of every function explicitly identified. This helps avoid bugs by ensuring any calling functions are expecting and properly using the return value of the function. It also forces a developer to seriously think about the consequences of changing the return signature of the function. If they just let type inference happen, they might not go investigate what impacts their change might have.

With React, there are several ways to solve this problem. In the sample above, ESLint helpfully displays the inferred return value of the function.  So, one possible fix to this problem is:


```
export default function App(): JSX.Element | null {...}
```


This code explicitly tells TypeScript what to expect this function to return.

Another possible refactor depending on style preferences is:


```
export const App: React.FunctionComponent = () => { ... }
export default App;
```


So, let's go through finding all these `Missing return type` issues and solve them like


```
export default function ???(): JSX.Element | null {...}
```


We're not done with missing return types yet 😢.


```
/Users/jn/Documents/Apps/expo-lint/components/Themed.tsx
7:8 warning Missing return type on function @typescript-eslint/explicit-module-boundary-types
```


This one's a bit different as this is just a function. We need to explicitly type the return as a `string` like:


```
export function useThemeColor(
  props: { light?: string; dark?: string },
  colorName: keyof typeof Colors.light & keyof typeof Colors.dark,
): string {...}
```


And another similar one:


```
/Users/jn/Documents/Apps/expo-lint/hooks/useColorScheme.web.ts
  3:16 warning Missing return type on function @typescript-eslint/explicit-module-boundary-types
```


Again, this one is a bit different. Let's look at this code:


```
// useColorScheme from react-native does not support web currently. You can replace
// this with react-native-appearance if you would like theme support on web.
export default function useColorScheme() {
  return "light";
}
```


We _could_ fix it just like this:


```
export default function useColorScheme(): string {
  return "light";
}
```


However, since this function literally can't return anything other than `light`, we can fix this linting issue more precisely with a "[string literal](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types)".


```
export default function useColorScheme(): "light" {
  return "light";
}
```

### Component Definition is Missing Display Name

Our next big issue is about the `navigation/BottomTabNavigator.tsx` missing a display name.



![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-9.png)



The [`react/display-name` warning](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/display-name.md) hints that the code in question is going to make debugging more difficult. When using the React debugging tools, it's very helpful for every component to have a name. This makes it easier to "see" what code is generated and search for a component in the debugger.

Normally, a developer doesn't have to provide a "display name" to their components. React automatically derives it based on the variable assigned to it.

However, in this file, the bottom tabs icon is generated in an anonymous, inline function. React can't make up a display name for it - hence the warning.

To solve this, we need to get rid of those inline functions and replace them with a component as follows:


```
const BottomTabTabBarIcon = ({ color }: { color: string }) => (
  <TabBarIcon name="ios-code" color={color} />
);
```

```
export default function BottomTabNavigator(): JSX.Element {
  const colorScheme = useColorScheme();

  return (
    <BottomTab.Navigator
      initialRouteName="TabOne"
      tabBarOptions={{ activeTintColor: Colors[colorScheme].tint }}
    >
      <BottomTab.Screen
        name="TabOne"
        component={TabOneNavigator}
        options={{
          tabBarIcon: BottomTabTabBarIcon,
        }}
      />
      <BottomTab.Screen
        name="TabTwo"
        component={TabTwoNavigator}
        options={{
          tabBarIcon: BottomTabTabBarIcon,
        }}
      />
    </BottomTab.Navigator>
  );
}
```


Tada! Problem solved!



![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-10-problem-solved.gif)


### Module is Not Defined

We've got yet another problem with the `babel.config.js` file. ESLint is warning:


```
/Users/jn/Documents/Apps/expo-lint/babel.config.js
  1:1 error 'module' is not defined no-undef
```


There are several solutions to this, but this file is not very irrelavant to our code; so, we're going to just ignore it like this:


```
// eslint-disable-next-line no-undef
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
  };
};
```

### React/No-Unescaped-Entities

Another issue in our `yarn lint` output:


```
/Users/jn/Documents/Apps/expo-lint/components/EditScreenInfo.tsx
   9:16 warning Missing return type on function @typescript-eslint/explicit-module-boundary-types
  42:39 error `'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;` react/no-unescaped-entities
```


Let's focus on the `react/no-unescaped-entities` part of this. When writing HTML, developers should avoid the use of apostrophe's, quotes, etc. "But..... this is a React Native project!", you might exclaim. That's true, but thanks to Expo for Web (via [React Native Web](https://github.com/necolas/react-native-web)), our React Native project can also run in the browser. So, we need to be good citizens and fix our code.

Replace:


```
Tap here if your app doesn't automatically update after making changes
```


with:


```
Tap here if your app doesn&apos;t automatically update after making changes
```


There are several such issues throughout the app; so, go tackle them. This escaping works just fine in React Native views as well.

Finally, run another `yarn lint` and 💥! No more linting errors!



![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-10-a.png)


## Commit All The Thingz!


![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-11-all-the-thingz.gif)



We're **finally** done! We fixed all the linting errors.

Before committing, let's make one intentional mistake to prove our pre-commit hook prevents the commit.  In `App.tsx`, add this:


```
const a = 1;
```


This should cause the pre-commit hook to fail because the variable `a` is defined but never used, right?


```
git add .
git commit -m"cleanup all the code!"
```


![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-12-1.png)


### Prevent Commits with ESLint Warnings

Husky let us down 🤦‍♂️!



![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-13-husky-let-us-down.gif)



What the heck?? Our pre-commit hook worked, prettified all the files, but it let our warning go through and commit?

The problem is that ESLint doesn't consider "warnings" as fatal errors; so, `yarn lint` runs and quits with an exit code of 0.  So, Husky thinks, "Looks good to me!"

We do **NOT** want this. The whole goal of this process is to enforce coding standards. If a developer can commit code with tons of warnings, we haven't really solved any problems. In fact, it can lead to buggy code if you attempt to use techniques like [type safe switch statements](https://justinnoel.dev/2019/10/28/2019-10-27-type-safe-switch-statements-with-typescript/).

To fix this, we need to make ESLint consider "warnings" as fatal errors. Modify the `lint` script in `package.json` to :


```
"lint": "eslint . --ext .ts,.tsx,.js,.jsx,.json --max-warnings 0",`
```


Now, we're explicitly telling ESLint that warnings should not be allowed. When it encounters any warnings, it will exit with a status code other than `1` and our pre-commit hook will reject the commit.

Let's test to make sure it works.

Again, in `App.tsx`, change `const a = 1;` to something like `const a = "1";`.

Then,


```
git add App.tsx
git commit -m"Don't fail me now, little doggy!"
```


![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-14.png)



Woohoo! Husky saw there was a problem with the code and rejected the commit!

Now, just delete that offending code, recommit and you're done.

## My Commits are So Slow 😡!!!

As your project grows, you'll add more and more files. When you commit, lint and prettier are going to examine every one of those files for issues. You can imagine in a very large project, it can take several seconds for your commits to get accepted. This gets really irritating, really fast.

Let's fix this by adding [lint-staged](https://github.com/okonet/lint-staged) to our project. With lint-staged, only the files we've touched and staged for commit will go through the linting process and be evaluated in the pre-commit hook.  This will drastically speed up your flow.

Install and configure lint-staged as follows:


```
npx mrm lint-staged
```


By default, the installation process modified our Husky config in `package.json` to:


```
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
```


and added this:


```
  "lint-staged": {
    "*.{ts,tsx,js,jsx,json}": "eslint --cache --fix"
  }
```


There are 3 issues with this:

1. I don't like automated fixes. So, I remove the `--fix`. I feel it's important for the developers to be warned about their mistakes and fix them manually.
2. It removed our `--max-warnings 0` flag; so, no ESLint warnings can make their way back into our code
3. It removed our automatic prettification 😞

So, let's modify the `lint-staged` section of `package.json` to get it back the way we like:


```
"lint-staged": {
  "*.{json,js,jsx,ts,tsx,css,scss,md}": "prettier --config .prettierrc --write",
  "*.{ts,tsx,js,jsx,json}": "eslint --cache --max-warnings 0"
}
```


Now, all commit attempts will run prettier and linting checks on any files we're attempting to commit and no one can commit bad  code anymore! Huzzah!

Here's what a commit looks like now:



![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/expo-lint-15.png)


## Danger Will Robinson!


![Enforcing Consistent and Error Free Code in an Expo React Native Project with TypeScript](https://justinnoel.dev/content/images/2020/07/es-lint-15-warning-will-robinson-1.gif)



Developers are human. Sometimes, we get in a rush, stuck on a problem, or just need to "Ship It!".  So, we sometimes try to bypass the rules to get something out the door. Sometimes, we just need to commit code even though we know it's got warnings to get it pushed up for another developer to work on.

Be aware that anyone on your team can do the following to bypass the pre-commit hooks:


```
git add .
git commit -m"Damn the torpedoes. Full steam ahead!" --no-verify
```


Now, it's important to add something into your PR process (GitHub Actions, CI/CD tests, etc) to make sure this code doesn't actually get merged until it's fixed.

I sure hope this walk through helps your team get on the path of clean, consistent code. If you have suggestions or questions, ping me on [Twitter](https://twitter.com/JustinNoelDev).