Boas Práticas em TypeScript: Escrevendo Código Mais Seguro e Fácil de Manter
Aprenda os padrões de TypeScript que mais importam — modo estrito, utility types, generics, discriminated unions e como evitar as principais armadilhas do sistema de tipos.
O TypeScript deixou de ser opcional e se tornou essencial na maioria dos projetos JavaScript profissionais. Mas há uma grande diferença entre "tecnicamente TypeScript" e "TypeScript que realmente pega bugs". Essa diferença se resume a um conjunto de padrões que desbloqueiam o verdadeiro poder do sistema de tipos.
Ative o modo estrito — sempre
A mudança de configuração de TypeScript com maior retorno:
// tsconfig.json
{
"compilerOptions": {
"strict": true
}
}
strict ativa um conjunto de verificações:
strictNullChecks—nulleundefinednão podem ser atribuídos a outros tiposnoImplicitAny— variáveis não podem ter tipoanyimplicitamentestrictFunctionTypes— verificação mais rigorosa de parâmetros de funçõesstrictPropertyInitialization— propriedades de classe devem ser inicializadas
Sem strictNullChecks, o TypeScript deixa passar a classe mais comum de erros em tempo de execução. Ative-o desde o primeiro dia — aplicá-lo retroativamente em uma base de código grande é muito trabalhoso.
Evite any — use unknown em vez disso
any é uma válvula de escape do sistema de tipos que silencia todos os erros e anula o propósito do TypeScript:
// ❌ any desativa toda a verificação de tipos
function processData(data: any) {
data.nonExistentMethod(); // Sem erro! Quebra em tempo de execução.
}
// ✅ unknown obriga você a restringir o tipo antes de usá-lo
function processData(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // Seguro — restringido para string
}
if (data instanceof Error) {
console.log(data.message); // Seguro — restringido para Error
}
}
unknown é a versão type-safe de any. Ele exige que você verifique o que tem antes de usar. Use-o para respostas de API, parsing de JSON e qualquer coisa vinda de fontes externas.
Discriminated unions para tratamento exaustivo
Um dos padrões mais poderosos do TypeScript:
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: User[] };
type ErrorState = { status: "error"; message: string };
type ApiState = LoadingState | SuccessState | ErrorState;
function render(state: ApiState) {
switch (state.status) {
case "loading":
return <Spinner />;
case "success":
return <UserList users={state.data} />; // state.data disponível aqui
case "error":
return <ErrorMessage msg={state.message} />; // state.message disponível aqui
default:
// TypeScript gerará erro aqui se você adicionar um novo estado sem tratá-lo
const _exhaustive: never = state;
throw new Error(`Unhandled state: ${_exhaustive}`);
}
}
A atribuição never no final é a verificação de exaustividade — se você adicionar uma nova variante em ApiState sem tratá-la no switch, o TypeScript gera um erro em tempo de compilação. Essa é uma das redes de segurança mais valiosas de toda a linguagem.
Utility types
O TypeScript vem com transformadores de tipos embutidos:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
}
// Torna todas as propriedades opcionais
type PartialUser = Partial<User>;
// Torna todas as propriedades obrigatórias
type RequiredUser = Required<User>;
// Torna todas as propriedades somente leitura
type ReadonlyUser = Readonly<User>;
// Seleciona propriedades específicas
type UserPreview = Pick<User, "id" | "name">;
// Exclui propriedades específicas
type UserWithoutDates = Omit<User, "createdAt">;
// Cria um tipo a partir dos valores de um objeto
type UserRole = User["role"]; // "admin" | "editor" | "viewer"
// Torna propriedades anuláveis
type NullableUser = { [K in keyof User]: User[K] | null };
// Tipo Record: objeto com tipos específicos de chave e valor
type RolePermissions = Record<UserRole, string[]>;
// Extract/Exclude de tipos union
type AdminOrEditor = Extract<UserRole, "admin" | "editor">; // "admin" | "editor"
type NonAdmin = Exclude<UserRole, "admin">; // "editor" | "viewer"
Generics para código reutilizável e type-safe
Generics permitem que você escreva funções e classes que funcionam com qualquer tipo, mantendo total segurança de tipos:
// Sem generics — perde informação de tipo
function first(arr: any[]): any {
return arr[0];
}
// Com generics — preserva o tipo
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // tipo: number | undefined
const str = first(["a", "b"]); // tipo: string | undefined
Restringindo generics
// T deve ter uma propriedade 'id'
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// K deve ser uma chave de T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name"); // tipo: string
const role = getProperty(user, "role"); // tipo: string
// getProperty(user, "missing"); // ❌ Erro TypeScript — "missing" não é uma chave
Type guards e narrowing
// Type predicate — type guard personalizado
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value
);
}
// Uso
const data: unknown = await fetchUser();
if (isUser(data)) {
console.log(data.name); // TypeScript sabe que data é User aqui
}
Para validação de respostas de API em tempo de execução, combine interfaces TypeScript com validação de schema em runtime usando Zod. Gere schemas Zod diretamente do seu JSON com nosso conversor JSON to Zod — cole um exemplo de resposta JSON e obtenha um schema type-safe instantaneamente.
Template literal types
TypeScript 4.1+ suporta manipulação de strings no nível de tipos:
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSKey = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"
Operador satisfies (TypeScript 4.9+)
Valida que um valor corresponde a um tipo sem alargá-lo:
type ColorMap = Record<string, string>;
// ❌ Sem satisfies — tipo alargado para Record<string, string>
const colors: ColorMap = {
primary: "#3b82f6",
secondary: "#8b5cf6",
};
colors.primary.toUpperCase(); // Funciona, mas perde o tipo string
// ✅ Com satisfies — valida E preserva os tipos literais
const colors = {
primary: "#3b82f6",
secondary: "#8b5cf6",
} satisfies ColorMap;
colors.primary.toUpperCase(); // ✅ TypeScript sabe que é uma string
colors.nonexistent; // ❌ Erro — chave não existe
Gerando tipos a partir de dados
Em vez de escrever interfaces TypeScript manualmente a partir de respostas de API:
- Cole um exemplo de resposta JSON em nosso conversor JSON to TypeScript para obter interfaces instantaneamente
- Ou gere um schema Zod com JSON to Zod — o Zod infere tipos TypeScript automaticamente via
z.infer<typeof schema>
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
});
// Tipo inferido automaticamente — sem duplicação
type User = z.infer<typeof UserSchema>;
// Valida em tempo de execução E fornece tipos TypeScript
const user = UserSchema.parse(await response.json());
Anti-padrões comuns a evitar
| Anti-padrão | Problema | Solução |
|---|---|---|
as any |
Desativa a verificação de tipos | Use unknown + narrowing |
// @ts-ignore |
Silencia erros sem corrigir | Corrija o erro de tipo |
as SomeType (cast inseguro) |
Ignora a inferência | Use type guards |
interface para unions |
Não funciona para discriminated unions | Use type |
object genérico demais |
Sem estrutura | Defina uma interface adequada |
Function como tipo |
Sem informação de parâmetros/retorno | Use uma assinatura específica |
Configurações de TypeScript que vale conhecer
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true, // array[n] retorna T | undefined
"exactOptionalPropertyTypes": true, // distingue propriedade ausente de undefined
"noImplicitReturns": true, // todos os caminhos de código devem retornar
"noFallthroughCasesInSwitch": true, // sem fallthrough acidental em switch
"verbatimModuleSyntax": true // import type explícito para imports somente de tipo
}
}
O valor do TypeScript é proporcional ao rigor com que você o utiliza. Os padrões apresentados aqui — modo estrito, discriminated unions, generics, verificações de exaustividade — são o que diferenciam um TypeScript que encontra bugs de um TypeScript que apenas adiciona sintaxe.