Pular para o conteúdo
TypeScript

Tipar Variáveis de Ambiente no TypeScript

Chega de process.env retornando string | undefined e quebrando em produção. Veja como tipar, validar e proteger suas variáveis de ambiente com TypeScript.

Por que isso é importante

Tipar Variáveis de Ambiente no TypeScript. Chega de process.env retornando string | undefined e quebrando em produção. Veja como tipar, validar e proteger suas variáveis de ambiente com TypeScript.

O problema com process.env no TypeScript

Por padrão, toda variável de ambiente no Node.js retorna string | undefined. Isso significa que o TypeScript não faz ideia se DATABASE_URL, API_KEY ou qualquer outra env existe de fato no seu ambiente. Você pode digitar process.env.DATABSE_URL (com typo) e o compilador não reclama nada.

Galera, o cenário piora quando o projeto cresce. Você tem envs diferentes em dev, staging e produção. Alguém esquece de adicionar uma variável no .env do servidor novo e tudo quebra em runtime. Dá pra resolver isso de forma simples e elegante com TypeScript.

// Sem tipagem - perigoso
const dbUrl = process.env.DATABASE_URL;
// tipo: string | undefined
// typo passa despercebido:
const dbUrl2 = process.env.DATABSE_URL; // sem erro!

Solução 1: Arquivo env.d.ts com declare global

A forma mais direta de tipar variáveis de ambiente é criar um arquivo de declaração de tipos. Você diz pro TypeScript exatamente quais envs existem e de que tipo são. Com isso, o autocomplete funciona e typos viram erros de compilação.

  1. Crie o arquivo env.d.ts na raiz do projeto (ou dentro de src/types/)
  2. Use declare global para estender o namespace NodeJS.ProcessEnv
  3. Liste todas as variáveis de ambiente do projeto com seus tipos
  4. Garanta que o arquivo está incluído no tsconfig.json
// env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: 'development' | 'production' | 'test';
      DATABASE_URL: string;
      API_KEY: string;
      PORT?: string;
      REDIS_URL?: string;
    }
  }
}

export {};

Agora o TypeScript sabe que DATABASE_URL é string (obrigatória) e PORT é string | undefined (opcional). Qualquer variável não listada gera erro. Simples assim.

Solução 2: Validação com Zod na inicialização

Tipar com declare global é bom, mas não valida se as variáveis existem de fato em runtime. O TypeScript confia que estão lá, porém se alguém esquecer de preencher o .env, o app vai quebrar em algum momento aleatório. Com Zod, você valida tudo na hora que o app sobe.

// src/env.ts
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
  PORT: z.coerce.number().default(3000),
  REDIS_URL: z.string().url().optional(),
});

// Valida e exporta as envs tipadas
export const env = envSchema.parse(process.env);

// Tipo inferido automaticamente
export type Env = z.infer<typeof envSchema>;

Se faltar qualquer variável obrigatória, o Zod joga um erro claro no momento da inicialização, não em runtime aleatório. E o tipo é inferido do schema, então você não precisa manter tipagem duplicada.

// Uso em qualquer lugar do projeto
import { env } from '@/env';

// Autocomplete completo + tipo correto
const port = env.PORT; // number (não string!)
const dbUrl = env.DATABASE_URL; // string

Solução 3: dotenv + tipagem manual

Se você não quer adicionar Zod como dependência, dá pra combinar dotenv com uma função de validação manual. A ideia é criar um helper que carrega as envs e lança erro se algo estiver faltando.

// src/config.ts
import 'dotenv/config';

function getEnv(key: string): string {
  const value = process.env[key];
  if (!value) {
    throw new Error(`Variável de ambiente ${key} não definida`);
  }
  return value;
}

function getEnvOptional(key: string, fallback: string): string {
  return process.env[key] ?? fallback;
}

export const config = {
  nodeEnv: getEnv('NODE_ENV'),
  databaseUrl: getEnv('DATABASE_URL'),
  apiKey: getEnv('API_KEY'),
  port: Number(getEnvOptional('PORT', '3000')),
  redisUrl: process.env.REDIS_URL, // opcional
} as const;

Essa abordagem é mais leve. Não precisa de dependência extra, e o config object fica fortemente tipado. O ponto negativo é que você precisa manter a validação manual.

Tipando env no Next.js

No Next.js a coisa muda um pouco. Variáveis prefixadas com NEXT_PUBLIC_ ficam disponíveis no browser, e as sem prefixo só existem no server. Isso impacta diretamente a tipagem.

// env.d.ts para Next.js
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      // Server-only
      DATABASE_URL: string;
      API_SECRET: string;

      // Public (acessível no browser)
      NEXT_PUBLIC_API_URL: string;
      NEXT_PUBLIC_GA_ID?: string;
    }
  }
}

export {};
// src/env.ts - Validação separada para server e client
import { z } from 'zod';

const serverSchema = z.object({
  DATABASE_URL: z.string().url(),
  API_SECRET: z.string().min(1),
});

const clientSchema = z.object({
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_GA_ID: z.string().optional(),
});

// Valida server envs (só roda no server)
export const serverEnv = serverSchema.parse(process.env);

// Valida client envs (roda em ambos)
export const clientEnv = clientSchema.parse({
  NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
});

Essa separação garante que variáveis secretas nunca vazem pro client e que as públicas estejam sempre validadas em ambos os lados.

Erros comuns ao tipar variáveis de ambiente

Atenção

Erro 1: Esquecer o export {} no final do env.d.ts. Sem isso, o arquivo não é tratado como module e o declare global não funciona corretamente.

Erro 2: Confiar que a tipagem garante existência em runtime. O declare global só ajuda em compile time. Sem validação com Zod ou helper manual, envs faltantes só quebram em produção.

Erro 3: Usar process.env direto espalhado pelo código inteiro. Centralize num arquivo config.ts ou env.ts. Isso facilita manutenção e testes.

Erro 4: No Next.js, acessar variáveis server-only no client. Envs sem prefixo NEXT_PUBLIC_ são undefined no browser, e o TypeScript não avisa.

Passo a passo: setup completo

  1. Instale as dependências: npm install zod dotenv
  2. Crie o arquivo src/env.ts com o schema Zod das suas envs
  3. Crie o arquivo env.d.ts na raiz para autocomplete global
  4. Adicione env.d.ts no include do tsconfig.json
  5. Importe env ao invés de process.env em todo o código
  6. Crie um .env.example listando todas as variáveis sem valores sensíveis
  7. Adicione .env no .gitignore se ainda não estiver lá

Exemplo completo: projeto Node.js com Express

// .env
NODE_ENV=development
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
API_KEY=sk-abc123
PORT=3000

// src/env.ts
import { z } from 'zod';
import 'dotenv/config';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
  PORT: z.coerce.number().default(3000),
});

export const env = envSchema.parse(process.env);

// src/server.ts
import express from 'express';
import { env } from './env';

const app = express();

app.listen(env.PORT, () => {
  console.log(`Server rodando na porta ${env.PORT}`);
  console.log(`Ambiente: ${env.NODE_ENV}`);
});

Checklist: variáveis de ambiente tipadas

Checklist: Env Tipado

  • Arquivo env.d.ts criado com declare global
  • Schema Zod definido com todas as envs obrigatórias e opcionais
  • Validação roda na inicialização do app (não em runtime tardio)
  • process.env não é acessado diretamente no código (só via env.ts)
  • .env.example criado e commitado no repositório
  • .env está no .gitignore
  • Envs de server e client separadas (Next.js)
  • Testes usam envs mockadas, não dependem de .env real

TypeScript Profissional: Projeto Completo

No CrazyStack você constrói um projeto completo com TypeScript

Node.js e React. Do setup até o deploy

com tipagem forte desde o primeiro arquivo. Acesse:

/comprar?src=blog-como-tipar-env-typescript
]
}
]
}
]
}