Todo componente de UI expõe uma API. Props, slots, eventos, contexto. Mas a maioria das APIs de componentes não foi projetada — foi acumulada.
Começou com três props. Virou quinze. Alguns estados são impossíveis de representar, outros são tecnicamente válidos mas semanticamente absurdos. A documentação não acompanhou. Ninguém sabe mais o que é suportado de verdade.
Esse é o sintoma de um componente sem contrato.
O que é um contrato de componente
Um contrato define:
- O que o componente aceita — não só os tipos, mas as combinações válidas
- O que ele promete fazer — comportamento garantido dado um input válido
- O que ele nunca fará — as fronteiras explícitas
// API sem contrato — tecnicamente válida, semanticamente ambígua
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive'
size?: 'xs' | 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
iconLeft?: ReactNode
iconRight?: ReactNode
fullWidth?: boolean
// ... 8 props mais
}
// Com contrato — combinações impossíveis são impossíveis de expressar
type ButtonProps =
| { variant: 'destructive'; requiresConfirmation: true; onConfirm: () => void }
| { variant: Exclude<ButtonVariant, 'destructive'> }
No segundo caso, o TypeScript impede que o consumidor construa um estado inválido. O componente não precisa validar em runtime — o compilador garante em design time.
Estados impossíveis devem ser impossíveis de representar
Esse princípio, popularizado por Richard Feldman no contexto de Elm, é extremamente útil para design systems.
Se um <Input> pode estar em estado de erro, ele precisa de uma errorMessage. Se não tem mensagem, não está em erro. Essas duas verdades precisam estar no tipo:
type InputProps =
| { error: true; errorMessage: string }
| { error?: false; errorMessage?: never }
Agora qualquer <Input error={true} /> sem errorMessage é um erro de compilação.
Documentação como parte do contrato
Tipos resolvem o que o compilador pode verificar. Mas um contrato completo também precisa de intenção documentada.
Três perguntas que todo componente deveria responder:
1. Quando usar este componente? Não “o que ele faz” — isso os tipos respondem. Mas quando ele é a escolha certa versus uma alternativa.
2. Quais estados visuais existem? Mapeados e documentados, não descobertos por tentativa e erro.
3. O que nunca deve ser feito com ele? As antipatterns explícitas são tão importantes quanto os exemplos positivos.
O custo de não ter contratos
Sem contratos, o custo se distribui pelos consumidores:
- Times que usam o componente precisam ler o código-fonte para entender o comportamento real
- Bugs aparecem em combinações de props que “funcionam” mas não foram testadas
- Refatorações ficam perigosas porque ninguém sabe quem depende do quê
- O design system vira um lugar de ansiedade, não de confiança
Com contratos bem definidos, o design system funciona como infraestrutura: invisível quando funciona, confiável quando precisam.
Por onde começar
Não é necessário contratar todos os componentes de uma vez. Comece pelos mais usados e mais problemáticos.
Para cada componente, faça três perguntas:
- Quais combinações de props são inválidas hoje mas não estão impedidas?
- Qual estado o designer assume que existe mas o desenvolvedor não implementou?
- Se esse componente sumisse amanhã, o que quebraria e o que ninguém perceberia?
As respostas revelam onde os contratos estão faltando — e onde o design system ainda está agindo como biblioteca.