React Native: Generating TypeScript Types for Environment Variables
Tags: typescript, reactnative, node, javascript
Published: 2020-04-19 10:02:01

---
title: 'React Native: Generating TypeScript Types for Environment Variables'
description: Get that automatic code completion.
tags: typescript,react-native,nodejs,javascript
published: true
---
As a React Native developer, I use [react-native-config](https://github.com/luggit/react-native-config) to manage different environments. I create `.env`, `.env.staging`, and `.env.prod` for development, staging and production at the root of my project.

Assuming my .env file looks like:

```
BASE_URL=https://localhost:8000
```

Then I'm able to do:

```js
import BuildConfig from "react-native-config"

console.log(BuildConfig.BASE_URL)
// https://localhost:8000
```

Seems good. Works fine. But not for me. There is no autocomplete. It's not typesafe. It's prone to human error that is only noticable it at runtime. 

Whenever I go back to native development with Android Studio I'd get jealous of that typesafe autocomplete. How can we get something like that for React Native?

Let's get some understanding of how it works for Android first. [Gradle](https://gradle.org/) is the build tool used for android's build system. Whenever the android app is built, a class is generated describing environment variables allowing for typesafe environment variable access.

Here is an illustration:

<img src="https://osamaq.com/img/android_buildconfig.png" width="100%" />

To bring that experience to React Native, we need to make a type declaration file that describes our environment variables module. That will let typescript know how to autocomplete. With a single environment variable, it will look like this:

```ts
// .env
declare module "react-native-config" {
  interface Env {
    BASE_URL: "https://localhost:8000"
  }

  const BuildConfig: Env

  export default BuildConfig
}
```

Now once we import react-native-config module, we should get autocomplete.

<img src="https://osamaq.com/img/env_autocomplete.png" width="100%" />

But that's not as good. We don't want to have to update our type declaration file manually!

For that, I resorted to writing quite a lengthy Node.js script. In _cough-cough_ plain javascript:

```js
const fs = require("fs")

const contents = () => {
  const env = fs.readFileSync(".env", { encoding: "ASCII" })
  const envStaging = fs.readFileSync(".env.staging", { encoding: "ASCII" })
  const envProd = fs.readFileSync(".env.prod", { encoding: "ASCII" })

  const envLines = env.split("\n")
  const envStagingLines = envStaging.split("\n")
  const envProdLines = envProd.split("\n")

  let filteredEnv = []
  let filteredEnvStaging = []
  let filteredEnvProd = []

  // Assumption: all files have the same number of lines
  for (let index = 0; index < envLines.length; index++) {
    const envLine = envLines[index]
    const envStagingLine = envStagingLines[index]
    const envProdLine = envProdLines[index]

    if (envLine.includes("=")) {
      if (envLine.includes("#")) {
        filteredEnv.push(envLine.split("#")[1].trim())
      } else {
        filteredEnv.push(envLine.trim())
      }
    }

    if (envStagingLine.includes("=")) {
      if (envStagingLine.includes("#")) {
        filteredEnvStaging.push(envStagingLine.split("#")[1].trim())
      } else {
        filteredEnvStaging.push(envStagingLine.trim())
      }
    }

    if (envProdLine.includes("=")) {
      if (envProdLine.includes("#")) {
        filteredEnvProd.push(envProdLine.split("#")[1].trim())
      } else {
        filteredEnvProd.push(envProdLine.trim())
      }
    }
  }

  return [filteredEnv, filteredEnvProd, filteredEnvStaging]
}

const generate = () => {
  const [filteredEnv, filteredEnvProd, filteredEnvStaging] = contents()
  let envVariableNamesArray = []
  let envVariableValuesArray = []

  for (let i = 0; i < filteredEnv.length; i++) {
    // Assumption: the files we read are not just comments
    const envPair = filteredEnv[i].split("=")
    const envStagingValue = filteredEnvStaging[i].split("=")[1]
    const envProdValue = filteredEnvProd[i].split("=")[1]

    envVariableNamesArray.push(envPair[0])

    envVariableValuesArray.push(envPair[1], envStagingValue, envProdValue)
  }

  // Assumption: for every name/key there are 3 values (env, env.staging, env.prod)
  let table = []
  let valuesCursor = 0

  for (let i = 0; i < envVariableNamesArray.length; i++) {
    table[i] = [envVariableNamesArray[i], []]

    const totalPushCount = 3
    let current = 0
    while (current !== totalPushCount) {
      const valueToPush = envVariableValuesArray[valuesCursor]

      if (!table[i][1].includes(valueToPush)) {
        table[i][1].push(valueToPush)
      }
      valuesCursor++
      current++
    }
  }

  const stringArrayMap = table.map((nameValueArray) => {
    const name = nameValueArray[0]
    const valuesArray = nameValueArray[1]

    let string = `${name}: `

    valuesArray.forEach((value, index) => {
      if (index === 0) {
        string = string.concat(`"${value}"`)
      } else {
        string = string.concat(` | "${value}"`)
      }
    })

    return string
  })

  const string = `declare module "react-native-config" {
  interface Env {
    ${stringArrayMap.join("\n    ")}
  }

  const Config: Env

  export default Config
}`

  fs.writeFileSync("env.d.ts", string, "utf8")
}

generate()
```

In summary, this script will read all 3 environment files and generate a `.env.d.ts` describing the types. It will only work if all 3 .env files contain the same number of variables with the same names, which makes sense.

At the root directory of my react native project, I created a scripts folder and placed it there. It looks like this `MyApp/scripts/generateEnvTypes.js`. Next I added the following npm script to my package.json:

`"generate-env-types": "node scripts/generateEnvTypes.js"`

Now, whenever I update my environment variables, I simply run the npm script and a new type declarations file is automatically generated! 🎉

![generate env types](https://raw.githubusercontent.com/osamaq/osamaq.com/master/static/img/generate_types.gif)

--------------------

PS: I'm maintaining a React Native [template with a lot of goodies](https://github.com/osamaq/react-native-template/) like the one in the article.