DEV Community

Osama Qarem
Osama Qarem

Posted on

React Native: Generating TypeScript Types for Environment Variables

As a React Native developer, I use 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
Enter fullscreen mode Exit fullscreen mode

Then I'm able to do:

import BuildConfig from "react-native-config"

console.log(BuildConfig.BASE_URL)
// https://localhost:8000
Enter fullscreen mode Exit fullscreen mode

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 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:

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:

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

  const BuildConfig: Env

  export default BuildConfig
}
Enter fullscreen mode Exit fullscreen mode

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

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:

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()
Enter fullscreen mode Exit fullscreen mode

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


PS: I'm maintaining a React Native template with a lot of goodies like the one in the article.

Top comments (1)

Collapse
 
aamnahakram profile image
Aamnah Akram

Neat! I was looking for Typescript autocomplete for .env as well :)

One minor adjustment to this would be replacing envProdLine.includes("#") with envProdLine.startsWith("#"). Comment lines start with # so it only makes sense to use startsWith(). Otherwise it'd break (say undefined ) for any values that have # in it.

if (envProdLine.includes("=")) {
  if (envProdLine.startsWith("#")) {
    filteredEnvProd.push(envProdLine.split("#")[1].trim())
  } else {
    filteredEnvProd.push(envProdLine.trim())
  }
}
Enter fullscreen mode Exit fullscreen mode