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.
- Crie o arquivo env.d.ts na raiz do projeto (ou dentro de src/types/)
- Use declare global para estender o namespace NodeJS.ProcessEnv
- Liste todas as variáveis de ambiente do projeto com seus tipos
- 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
- Instale as dependências: npm install zod dotenv
- Crie o arquivo src/env.ts com o schema Zod das suas envs
- Crie o arquivo env.d.ts na raiz para autocomplete global
- Adicione env.d.ts no include do tsconfig.json
- Importe env ao invés de process.env em todo o código
- Crie um .env.example listando todas as variáveis sem valores sensíveis
- 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
]
}
]
}
]
}