Flow: Using process.env variables with types

Created on 17 Dec 2015  路  18Comments  路  Source: facebook/flow

I'm working on a codebase that includes certain config values as environment variables and uses process.env to load them into code, for instance:

var x = process.env.ENDPOINT + '/foo';

How should I get this to work with Flow? Currently the code will fail with an error like

 35:   var url = process.env.MAPBOX_API_ENDPOINT + '/v4/' +
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ undefined. This type is incompatible with
 35:   var url = process.env.MAPBOX_API_ENDPOINT + '/v4/' +
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ string

I could write an interface for process, but don't want to overwrite the entire process. Trying to do that:

declare class Env {
  MAPBOX_API_ENDPOINT: string;
}

declare var process.env: Env;

results in an error:

interfaces/aws.js:23
 23: declare var process.env: Env;
                        ^ Library parse error:
 23: declare var process.env: Env;
                        ^ Unexpected token .

Thanks for any help!

Most helpful comment

Process.prototype.env is typed as env : { [key: string] : ?string }; because it's not guaranteed that any environment variable is set. you could test for its existence before using it, e.g.

const mapboxApiEndpoint = process.env.MAPBOX_API_ENDPOINT;
if (!mapboxApiEndpoint) { throw new Error(...); }
var url = mapboxApiEndpoint + '/v4/' + ...

All 18 comments

Process.prototype.env is typed as env : { [key: string] : ?string }; because it's not guaranteed that any environment variable is set. you could test for its existence before using it, e.g.

const mapboxApiEndpoint = process.env.MAPBOX_API_ENDPOINT;
if (!mapboxApiEndpoint) { throw new Error(...); }
var url = mapboxApiEndpoint + '/v4/' + ...

(i don't mean closing this to shut down conversation... feel free to reopen)

@mroch @tmcw I tried with:

const perfectenv = new Proxy(process.env, {
  get: function (env, key) /* : string */ {
    const value = env[key]
    if (value == null) {
      throw new Error(`Missing ${key} ENV var`)
    }
    return value
  }
})

const {
  PORT,
  CONSUMER_KEY,
  CONSUMER_SECRET,
  APP_URL
} = perfectenv

const TWT_OAUTH_URL = 'https://api.twitter.com/oauth'
const oauth = {
  callback: `${APP_URL}/oauth-lobby`,
  consumer_key: CONSUMER_KEY,
  consumer_secret: CONSUMER_SECRET
}

But I still get the null. This type cannot be added to string error.

It looks like Flow doesn't take into account Proxy get's return type.

I have tons of environment variables to check. How can one this in a DRY way? Preferably with some fancy destructering.

@mroch
It's sad that flow cannot understand assert, like in this example:

const assert = require('assert');
const mapboxApiEndpoint = process.env.MAPBOX_API_ENDPOINT;
assert(mapboxApiEndpoint!==null);
var url = mapboxApiEndpoint + '/v4/' + ... // flow doesn't get that mapboxApiEndpoint was checked

Only manual check will work...

@viskin there is a trick _(:sparkles:magic:sparkles:)_, you can use invariant instead of assert:

const invariant = require('assert');
const mapboxApiEndpoint = process.env.MAPBOX_API_ENDPOINT;
invariant(mapboxApiEndpoint);
var url = mapboxApiEndpoint + '/v4/';

but don't do that :laughing:

@madbence how is that any different? special treatment of variable name?

@dasilvacontin yup :sparkles:

@madbence
Cool, thanks. I see the invariant as described here https://github.com/facebook/flow/issues/112 is due to be removed

Bottom line, what solution is the preferable one to use environment variables with flow js?

i think the idiomatic way is something like this:

// throw
if (!process.env.FOO) throw new Error('FOO missing');
const foo = process.env.FOO;

// fall back
const bar = process.env.BAR || 'bar';

(foo: string); // ok!
(bar: string); // ok!

you can also declare something like this

class process {
    static env: { FOO: string }
}

I'm using dotenv-safe to make sure some process.env will not be null in my code

Is there any way to tell Flow to understand that some .env are string instead of ?string?

i'd recommend having a config.js file in your project root, where you can set up your app config, like this:

// @flow
require('dotenv-safe').load();
const env = ((process.env: any): {[string]: string}); // if you're sure that everything will be defined

export default {
  foo: env.FOO,
  bar: env.BAR,
  // ...
};

instead of the {[string]: string} you can use any shape, eg:

{
  FOO: string,
  BAR: string,
  [string]: ?string,
}

if you want only FOO and BAR to be defined.

great trick, I'll try to write a babel plugin to inject variables using .env.example values

I believe this is the only right approach:

1) We want explicit error as soon as possible, aka module loading.

That's all.

// @flow
/* global fetch:false */
import 'isomorphic-fetch';
import { Environment, Network, RecordSource, Store } from 'relay-runtime';

const appGraphqlEndpoint = process.env.APP_GRAPHQL_ENDPOINT;
if (appGraphqlEndpoint == null) throw new Error('Missing APP_GRAPHQL_ENDPOINT');

const createRelayEnvironment = (token: ?string, records: Object = {}) => {
  const store = new Store(new RecordSource(records));
  const network = Network.create((operation, variables) =>
    fetch(appGraphqlEndpoint, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        ...(token != null ? { authorization: `Bearer ${token}` } : null),
      },
      body: JSON.stringify({
        query: operation.text,
        variables,
      }),
    }).then(response => response.json()),
  );
  return new Environment({ store, network });
};

export default createRelayEnvironment;

@Andarist's solution is perfect for client side code where process.env.VAR_NAME is defined using webpack's DefinePlugin. For that specific use case always checking for existence of is repetitive and unnecessary.

To explain further, he was (most probably) referring to libdefs. For example, you can add a flow-typed/env.js file containing:

declare class process {
  static env: {
    API_URL: string,
  }
}

and process.env.API_URL work without checking, i.e. it will no longer be a maybe type.

In a Node.js environment, I'm looking for a clever way to typecheck (and also to check runtime) environement.

I'm pretty used to do something like this in vanillajs :

const envalid = require('envalid')

const envConfig = {
  PORT: envalid.port(),
  EDITO_URL: envalid.url(),
  TUNNEL_URL: envalid.url(),
  WORDPRESS_URL: envalid.url(),
  HOMEPAGE_URL: envalid.url(),
  WORDPRESS_HOST: envalid.str(),
  PMDC_VHOST: envalid.str(),
  REDIRECTS_FILE_PATH: envalid.str({ default: 'redirection.csv' }),
  PROXY_CORS: envalid.bool({ default: false }),
  PROXY_CACHE: envalid.bool({ default: false })
}

module.exports = function (envInput) {
  return envalid.cleanEnv(envInput, envConfig, {
    dotEnvPath: null
  })
}

And in app.js

// Monkey patch like a dumb
process.env = require('./check-env.js')(process.env)

which works pretty well runtime; and do some kind of type checks + parsing for me.
... even if patching process.env is not as clean as I would like.

Now I'm struggling at doing something clever to make this work with flow
I had the idea to make a singleton; but it basically doesn't work.

import envalid from 'envalid'

const envConfig = {
  PORT: envalid.port(),
  EDITO_URL: envalid.url(),
  TUNNEL_URL: envalid.url(),
  WORDPRESS_URL: envalid.url(),
  HOMEPAGE_URL: envalid.url(),
  WORDPRESS_HOST: envalid.str(),
  PMDC_VHOST: envalid.str(),
  REDIRECTS_FILE_PATH: envalid.str({ default: 'redirection.csv' }),
  PROXY_CORS: envalid.bool({ default: false }),
  PROXY_CACHE: envalid.bool({ default: false })
}

interface IConfig {
  PORT: number,
  EDITO_URL: string,
  TUNNEL_URL: string,
  WORDPRESS_URL: string,
  HOMEPAGE_URL: string,
  WORDPRESS_HOST: string,
  PMDC_VHOST: string,
  REDIRECTS_FILE_PATH: string,
  PROXY_CORS: boolean,
  PROXY_CACHE: boolean
}

let config: ?IConfig = null

export function init (envInput) : IConfig {
  config = envalid.cleanEnv(envInput, envConfig, {
    dotEnvPath: null
  })

  return config
}

export function get () : IConfig {
  if (!config) {
    throw new Error('Config should be initialized first, with init()')
  }

  return config
}

export default {
  init,
  get
}

a.js

import Config from './config.js'

const config = Config.get()

app.js

import a from 'a.js'

const config = Config.init(process.env)

Mostly because the side-effect init(process.env) is done after importing a.js which goes get()

Any ideas ?

Was this page helpful?
0 / 5 - 0 ratings