Typechecking de variáveis de ambiente em Node.js com TypeScript


Faz um tempinho que eu não escrevo aqui, mas hoje eu voltei. Voltei porque hoje configurei um servidor de node.js com typescript, e nele escrevi um sistema bem simples e util para fazer a verificação de tipagem das variaveis de ambiente, e é sobre ele que irei falar hoje.

O Código

Não vou ficar te enrolando com toda a história desse código, nem do projeto no qual estou trabalhando (isso é pra outro post). Se você só quiser o código, ele está logo abaixo. Se não entender algo ou só estiver curioso mesmo, fique à vontade para continuar lendo logo após o código.

import "dotenv/config";
import { z } from "zod";

const env = z.object({
    PORT: z.coerce.number().max(65535).min(0),
    NODE_ENV: z.enum(["development", "production"]),
});

export type Env = z.infer<typeof env>;

declare global {
    namespace NodeJS {
        export interface ProcessEnv {
            [key: string]: any | undefined
        }
        interface ProcessEnv extends Env { }
    }
}

env.parse(process.env);

Que bom que você chegou até aqui, pra começar, ultilizaremos duas bibliotecas: dotenv e zod. A primeira é para carregar as variaveis de ambiente do arquivo .env para o process.env, e a segunda é para fazer verificação de tipagem em qualquer objeto que você quiser. Os outros geralmente a usam para fazer verificação de tipagem em objetos que vem de uma API, mas eu a usei para fazer verificação de tipagem nas variaveis de ambiente.

Primeiro definimos o schema, que é o objeto que o zod usará para realizar a verificação de tipagem. Nele definimos todas as variaveis de ambiente que queremos verificar, e o tipo que elas devem ter. No meu caso, eu defini que a variavel PORT deve ser um numero entre 0 e 65535, e que a variavel NODE_ENV deve ser uma string que pode ser development ou production. Você pode definir qualquer tipo que quiser, desde que seja um tipo que o zod entenda.

const env = z.object({
    PORT: z.coerce.number().max(65535).min(0),
    NODE_ENV: z.enum(["development", "production"]),
});

Agora que vem a parada com o typescript, o zod oferece um ultilitario para convertermos um schema em um tipo do typescript, tipo esse que podemos usar para tipar qualquer objeto que queremos verificar. No meu caso, eu criei um tipo chamado Env que é o tipo do meu schema env.

export type Env = z.infer<typeof env>;

Lá vamos nós pra mais typescript, dessa vez um cadinho mais assustador, mas acredita em mim, é simples. o process.env do node usa a interface ProcessEnv para definir o tipo das variaveis de ambiente, e é nela que iremos injetar os tipos das variaveis de ambiente que temos. Mas tem um problema, a interface ProcessEnv por definição aceita apenas string e undefined como tipos, e nosso campo PORT sai como um tipo de number. Para podermos injetar nossos tipos no ProcessEnv precisamos redefini-lo para aceitar qualquer tipo de variavel, ou undefined (any | undefined).

declare global {
    namespace NodeJS {
        // Aqui redefinimos o tipo das variaveis de ambiente para aceitar qualquer tipo ou undefined
        export interface ProcessEnv {
            [key: string]: any | undefined
        }

        // Aqui injetamos nossos tipos
        interface ProcessEnv extends Env { }
    }
}

E por fim, a ultima parte do código, a parte que faz a verificação de tipagem. Aqui usamos o env.parse para fazer a verificação de tipagem das variaveis de ambiente, e se alguma variavel não estiver de acordo com o schema definido, o zod irá lançar um erro que irá terminar o programa. E é isso, simples assim.

env.parse(process.env);

Essa ultima parte é bem customizavel também, você pode fazer o que quiser com o erro que o zod lança, você pode formatar a mensagem bonitinha, enviar uma notificação pra todos os seus engenheiros, ou simplesmente deixar o programa terminar. Eu escolhi a ultima opção, pois não tenho necessidade de fazer nada mais complexo por agora.

você pode aprender mais sobre o zod em zod.dev (sério, é uma biblioteca muito boa, vale a pena dar uma olhada)