Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptClean Code

Tipagem Segura em TypeScript

unknown 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

anyunknown
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 usarNunca (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 runtime

A 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 erro

A 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ção

Frontend — 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érico

Backend — 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ção

4. 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 erro

Frontend — 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çãoUse
Dados de API externa, req.body, input do usuáriounknown + type guard
Dados que podem não existirT | null + checagem
Configurações com valores literaissatisfies
Strings com formato específicoTemplate literal types
Quase nuncaany

Ative sempre no tsconfig.json:

{
  "strict": true,
  "noUncheckedIndexedAccess": true
}

Referências

On this page