Tipagem Segura em TypeScript
Baixar PDFunknown vs any, strictNullChecks, satisfies, template literal types — a fundação de qualquer código TypeScript limpo.
Tipagem Segura em TypeScript
A tipagem é a primeira linha de defesa do Clean Code em TypeScript. Antes de pensar em padrões, SOLID ou arquitetura, você precisa garantir que o compilador está trabalhando para você, não contra você.
1. unknown sobre any
O problema: any desliga o compilador
any é o "modo JavaScript" do TypeScript. Quando você usa any, o compilador aceita qualquer coisa — e qualquer erro só aparece em runtime (quando o usuário já está usando).
// ❌ any — compila sem erro, quebra em runtime
function formatPrice(data: any) {
return data.price.toFixed(2); // Se data for string ou null → BOOM em produção
}
// O compilador aceita:
formatPrice(null); // ✅ compila — ❌ quebra em runtime
formatPrice("texto"); // ✅ compila — ❌ quebra em runtime
formatPrice({ preco: 10 }); // ✅ compila — ❌ quebra em runtime (é "preco", não "price")A solução: unknown força validação
unknown diz: "eu não sei o que é isso, então o compilador vai me obrigar a validar antes de usar".
// ✅ unknown — o compilador exige validação
function formatPrice(data: unknown): string {
if (typeof data === 'object' && data !== null && 'price' in data && typeof data.price === 'number') {
return data.price.toFixed(2);
}
throw new Error('Dados de preço inválidos');
}
// Frontend — resposta de API
async function fetchProduct(id: string): Promise<Product> {
const res = await fetch(`/api/products/${id}`);
const data: unknown = await res.json(); // ✅ nunca assuma que a API retorna o tipo certo
if (!isProduct(data)) {
throw new Error('Resposta da API não é um Product');
}
return data;
}
function isProduct(data: unknown): data is Product {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data &&
'price' in data
);
}
// Backend — body de requisição
app.post('/api/users', (req, res) => {
const body: unknown = req.body; // ✅ req.body é unknown por padrão no Express
if (!isCreateUserDto(body)) {
return res.status(400).json({ error: 'Body inválido' });
}
// Agora body é CreateUserDto — TS sabe disso
const user = userService.create(body);
res.json(user);
});Comparativo
any | unknown | |
|---|---|---|
| Aceita qualquer valor? | ✅ Sim | ✅ Sim |
| Permite chamar métodos sem validar? | ✅ Sim (perigoso) | ❌ Não (seguro) |
| Erro aparece quando? | Em runtime (produção) | Em compilação (antes de rodar) |
| Quando usar | Nunca (quase) | Dados externos: API, input do usuário, req.body |
2. strictNullChecks
O problema: null em qualquer lugar
Sem strictNullChecks, null e undefined são compatíveis com qualquer tipo. Isso significa que qualquer variável pode ser null sem aviso.
// Sem strictNullChecks — compila sem erro
function getUserName(user: User) {
return user.name.toUpperCase(); // Se user for null → Cannot read property 'name' of null
}
const user: User = null; // ✅ compila — sem aviso
getUserName(user); // ❌ quebra em runtimeA solução: strictNullChecks força tratamento
// Com strictNullChecks: true
function getUserName(user: User | null) {
return user.name.toUpperCase(); // ❌ ERRO: 'user' is possibly 'null'
}
// Força o tratamento:
function getUserName(user: User | null) {
if (!user) return 'Usuário desconhecido';
return user.name.toUpperCase(); // ✅ TS sabe que user não é null aqui
}Configuração
// tsconfig.json — SEMPRE habilite isso
{
"compilerOptions": {
"strict": true, // inclui strictNullChecks + strictFunctionTypes + etc
"noUncheckedIndexedAccess": true // array[i] retorna T | undefined
}
}Frontend — dados de API
// ✅ Com strictNullChecks, o TS força você a tratar loading e erro
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null); // pode ser null
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser)
.catch(e => setError(e.message));
}, [userId]);
// ❌ Sem checagem: TS erro — user pode ser null
// return <h1>{user.name}</h1>;
// ✅ Com checagem: TS sabe que user não é null dentro do if
if (error) return <div>Erro: {error}</div>;
if (!user) return <div>Carregando...</div>;
return <h1>{user.name}</h1>; // ✅ TS sabe: user é User, não User | null
}Backend — busca no banco
// ✅ O TS força você a tratar o caso "não encontrado"
async function getUser(id: string): Promise<User | null> {
return db.users.findById(id);
}
async function updateUser(id: string, data: UpdateUserDto) {
const user = await getUser(id);
// ❌ TS erro: user pode ser null
// user.name = data.name;
// ✅ TS aceita com checagem
if (!user) {
throw new NotFoundError(`Usuário ${id} não encontrado`);
}
user.name = data.name;
}3. Operador satisfies
O problema: perder inferência ao tipar
// ❌ Tipo explícito perde inferência de literais
const routes: Record<string, { path: string; method: string }> = {
getUser: { path: '/users/:id', method: 'GET' },
createUser: { path: '/users', method: 'POST' },
};
// routes.getUser.method é string — não 'GET'
// Se alguém usar routes.getUser.method === 'GET', funciona,
// mas routes.getUser.method === 'PUT' também aceita sem erroA solução: satisfies valida sem perder inferência
// ✅ satisfies — valida a estrutura, preserva os literais
type Route = { path: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE' };
const routes = {
getUser: { path: '/users/:id', method: 'GET' },
createUser: { path: '/users', method: 'POST' },
} satisfies Record<string, Route>;
// routes.getUser.method é inferido como 'GET' — literal type
// routes.getUser.path é inferido como '/users/:id' — literal type
// ❌ TS erro — 'PUT' não é compatível com 'GET'
routes.getUser.method = 'PUT'; // erro em compilaçãoFrontend — configuração de rotas
// ✅ Config de navegação type-safe
const navigation = {
main: [
{ label: 'Início', href: '/' },
{ label: 'Produtos', href: '/produtos' },
{ label: 'Contato', href: '/contato' },
],
footer: [
{ label: 'Termos', href: '/termos' },
{ label: 'Privacidade', href: '/privacidade' },
],
} satisfies Record<string, Array<{ label: string; href: string }>>;
// navigation.main[0].href é '/' — não string genérico
// navigation.main[0].label é 'Início' — não string genéricoBackend — configuração de ambiente
// ✅ Config de ambiente validada
const env = {
port: parseInt(process.env.PORT ?? '3000'),
dbUrl: process.env.DATABASE_URL ?? 'mongodb://localhost:27017/mydb',
jwtSecret: process.env.JWT_SECRET ?? 'dev-secret',
redisUrl: process.env.REDIS_URL ?? 'redis://localhost:6379',
} satisfies {
port: number;
dbUrl: string;
jwtSecret: string;
redisUrl: string;
};
// env.port é number — TS garante
// env.dbUrl é string — TS garante
// Se alguém adicionar uma propriedade sem tipo correto → erro em compilação4. Template Literal Types
O que são
Template literal types permitem criar tipos que são padrões de string. É como regex no sistema de tipos.
// Tipos básicos
type Color = `#${string}`; // '#ff0000', '#abc', mas não 'red'
type Email = `${string}@${string}`; // '[email protected]', mas não 'user'
type ISODate = `${number}-${number}-${number}`; // '2025-01-15'
// Variáveis validadas
const color: Color = '#ff0000'; // ✅
const bad: Color = 'red'; // ❌ TS erroFrontend — eventos e rotas dinâmicas
// Event handlers — garante que o handler existe para o evento
type HtmlEvent = `on${Capitalize<MouseEvent>}`;
// 'onClick' | 'onMouseEnter' | 'onFocus' | ...
// Rotas dinâmicas do Next.js
type ApiRoute = `/api/${string}`;
type MercadoRoute = `/mercado-financeiro/${string}/${string}`;
const route1: ApiRoute = '/api/users'; // ✅
const route2: ApiRoute = '/internal/users'; // ❌ TS erro
const route3: MercadoRoute = '/mercado-financeiro/fii/ativos/hgff11'; // ✅Backend — HTTP methods + paths
// Combinações type-safe de método + rota
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = `${HttpMethod} /api/${string}`;
const endpoint: Endpoint = 'GET /api/users'; // ✅
const bad: Endpoint = 'PATCH /api/users'; // ❌ TS erro — PATCH não existe
// CSS-in-JS prop names
type CSSProperty = `--${string}`;
const themeColor: CSSProperty = '--primary-color'; // ✅
const badColor: CSSProperty = 'primary-color'; // ❌ TS erro
// DOM event attributes
type EventAttribute = `on${Capitalize<string>}`;Regra geral
| Situação | Use |
|---|---|
Dados de API externa, req.body, input do usuário | unknown + type guard |
| Dados que podem não existir | T | null + checagem |
| Configurações com valores literais | satisfies |
| Strings com formato específico | Template literal types |
| Quase nunca | any |
Ative sempre no tsconfig.json:
{
"strict": true,
"noUncheckedIndexedAccess": true
}