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:

  1. Quais combinações de props são inválidas hoje mas não estão impedidas?
  2. Qual estado o designer assume que existe mas o desenvolvedor não implementou?
  3. 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.