interface vs type vs satisfies para props
TL;DR
Para props de componentes:
interfacequando você quer declaration merging (estender tipos de libs externas) ou está escrevendo uma lib que outros vão estender.typequando precisa de unions, intersections, mapped, conditional.satisfiespara validar props parciais sem perder literal types. Convenção: sufixoProps(ex:ButtonProps).
O que é
Os três construtores resolvem problemas distintos. Para detalhes da diferença em geral (não só em props), ver TypeScript na seção “Interfaces e Type aliases”.
interface define o shape de um objeto. Tem duas características que type não tem: declaration merging (duas declarações da mesma interface User { ... } no mesmo escopo se fundem em uma só) e a sintaxe extends para herança de shape. É a ferramenta canônica para tipos que outros vão estender — a partir do código próprio (extends) ou do código deles (re-declarando a mesma interface).
type é um alias para qualquer tipo. Aceita tudo que interface aceita, mais: unions (A | B), intersections (A & B), mapped types ({ [K in keyof T]: ... }), conditional types (T extends U ? X : Y), tuplas, template literals e renomear primitivos. Não suporta declaration merging — duas type Foo = ... no mesmo escopo dão erro de redeclaração.
satisfies (TS 4.9+) é um operador de validação. Verifica que um valor obedece a um tipo sem fazer widening — ao contrário da anotação :, que troca o tipo inferido pelo tipo declarado. Em props, aparece quando você quer um default partial ou um config que valida shape mas mantém os literais específicos de cada chave. A nota anterior (02) já cobriu satisfies em configs gerais; aqui o foco é o uso específico em props.
Por que importa
A escolha entre interface e type não é cosmética em três cenários concretos:
-
Componentes que estendem props de elementos HTML. Padrões como
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'>versustype ButtonProps = React.ComponentPropsWithoutRef<'button'> & { ... }parecem equivalentes — e em uso direto são — mas a versãointerfacepermite que outros estendamButtonPropspor sua vez sem ginástica sintática. Em design systems, isso vira ergonomia de API. -
Componentes que aceitam props variantes. Um
<Button>que muda quais props aceita conformevariant: 'icon' | 'text' | 'split'precisa de discriminated union. Unions só existem comtype. Tentar expressar isso cominterfacesimplesmente não compila. -
Componentes em libs públicas. Quem consome uma lib pode querer estender as props publicadas para criar um wrapper.
interfacepermite — declaration merging é a ferramenta mais flexível para extensibilidade entre módulos.typeforça quem consome a fazerMyProps & { extra: X }em cada call site.
A regra prática que se desprende: interface quando outros vão estender; type quando o tipo é “fechado” do meu lado. E satisfies quando preciso de um default que não pode perder literais.
Como funciona
Sample 1 — interface para extensão
// Quando quero permitir que outros estendam
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
variant?: 'primary' | 'secondary';
}
// Outra lib pode estender:
interface FancyButtonProps extends ButtonProps {
glow: boolean;
}interface ... extends lê de cima para baixo: ButtonProps herda tudo de ComponentPropsWithoutRef<'button'> (todas as props nativas do <button>) e adiciona variant. Em seguida, FancyButtonProps herda tudo de ButtonProps e adiciona glow. A cadeia é explícita, e quem lê o tipo entende a intenção sem precisar decifrar uma intersection longa.
Sample 2 — type para união de variants
// Discriminated union — props mudam conforme variant
type ButtonProps =
| { variant: 'icon'; icon: React.ReactNode; label: string }
| { variant: 'text'; children: React.ReactNode }
| { variant: 'split'; primary: React.ReactNode; secondary: React.ReactNode };
// TS força handler a verificar variant antes de acessar a prop específica
function Button(props: ButtonProps) {
if (props.variant === 'icon') return <span>{props.icon}</span>;
// props.children não existe aqui — narrowed
// ...
}Esse é o caso onde interface não serve: não há interface A | B. O discriminated union é construído com type e a propriedade variant faz o papel de discriminator — quando o handler verifica props.variant === 'icon', o TS estreita o tipo para o ramo correspondente da union, e só as props daquele ramo ficam acessíveis. É o pattern canônico para componentes com modos mutuamente exclusivos.
Sample 3 — satisfies para defaults parciais
const DEFAULT_BUTTON_PROPS = {
variant: 'primary',
size: 'md',
disabled: false,
} satisfies Partial<ButtonProps>;
// satisfies valida shape sem widening:
// DEFAULT_BUTTON_PROPS.variant: 'primary' (literal preservado)
// vs
// const X: Partial<ButtonProps> = { variant: 'primary', ... };
// X.variant: ButtonProps['variant'] | undefined (widened)A diferença é sutil mas importa: anotar com : Partial<ButtonProps> faz o tipo de DEFAULT_BUTTON_PROPS.variant virar ButtonProps['variant'] | undefined — você perde a informação de que aquele valor específico é literalmente 'primary'. satisfies valida o shape (acusa typos, exige que variant seja um valor válido de ButtonProps['variant']) e mantém o literal 'primary'. Útil para defaults que serão spread em JSX (<Button {...DEFAULT_BUTTON_PROPS} {...overrides} />) ou consultados em outros lugares com tipo preciso.
Sample 4 — convenção de nomenclatura
// Sufixo Props — convenção do ecossistema
type ButtonProps = { /* ... */ };
type ModalProps = { /* ... */ };
type FormProps = { /* ... */ };
// Não use I-prefix (anti-pattern em TS):
// interface IButtonProps { ... } // ruim — convenção C#/Java, não TSSufixo Props é convenção observada amplamente em projetos React+TS — cheatsheets, exemplos oficiais e libs do ecossistema usam o pattern. O prefixo I (IButtonProps) é herança de C#/Java e considerado anti-pattern em TS: a orientação oficial do TypeScript Handbook e a maioria das style guides recomendam evitar. O argumento é que TS usa structural typing (não nominal), então marcar visualmente “isto é uma interface” não comunica nada útil ao consumidor — o que ele precisa saber é o shape, não se o construtor foi interface ou type.
Na prática
Padrão observado em libs grandes do ecossistema React (cheatsheet oficial, exemplos do Handbook, documentação de várias libs maduras): type é a escolha default quando o tipo é “fechado” no lado da lib, com interface aparecendo em cenários onde extends ou declaration merging trazem ganho real (props públicas que serão estendidas por consumidores, integração com tipos de libs externas via merging). O React TypeScript Cheatsheet é explícito sobre isso: o exemplo canônico de Props usa type, com a ressalva “use interface if exporting so that consumers can extend”.
A regra prática derivada: interface quando outros vão estender; type quando o tipo é “fechado” do meu lado. Em apps de produto (não libs), type é mais simples e cobre 95% dos casos — o tipo das props de <UserProfile> raramente precisa ser estendido por outro código no mesmo app. Em libs públicas, interface em props exportadas dá flexibilidade para quem vai consumir a lib criar wrappers e variantes sem fricção.
satisfies entra em camadas adjacentes às props: defaults parciais, mapas de variantes (const VARIANTS = { primary: {...}, secondary: {...} } satisfies Record<Variant, Style>), tabelas de configuração que viram props via spread. Não é “como tipar props” no sentido estrito — é “como construir valores que viram props sem perder informação no caminho”.
Armadilhas
-
Misturar
interfaceetypeno mesmo codebase sem critério. Inconsistência local é pior que qualquer escolha. Se metade dos componentes usatype Propse a outra metade usainterface Propssem diferença justificável, revisores perdem tempo decidindo o que importa em cada PR. Defina a regra do projeto (a mais comum:typepor default,interfacequando precisa deextends/merging) e siga. -
Tentar union com
interface(não funciona).interface ButtonProps = { variant: 'icon' } | { variant: 'text' }simplesmente não compila —interfacenão tem sintaxe para union. Erro comum de quem está migrando para discriminated unions sem trocar o construtor. Sintoma:'=' expectedouAn interface can only extend an object type or intersection of object types with statically known members. Corrige trocandointerfaceportype. -
Esquecer que
interfacepermite declaration merging acidental. Se você declarainterface ButtonProps { ... }e em outro lugar do mesmo escopo (mesmo arquivo, mesmonamespace, mesmo módulodeclare-ado) outrainterface ButtonProps { ... }aparece, as duas se fundem silenciosamente. Em código próprio é raro, mas em projeto com vários arquivos de tipos globais (global.d.ts, ambient declarations) pode causar bugs sutis — o tipo público passa a ter props que o autor original nem sabe que estão lá.type ButtonProps = ...redeclarado dá erro imediato, o que é mais seguro como default. -
Usar
interfacepor hábito quando precisa de mapped/conditional types.interface PartialButtonProps = { [K in keyof ButtonProps]?: ButtonProps[K] }não compila — mapped types só existem emtype. Quando o tipo é “função de outro tipo” (Pick, Omit, Partial, custom mappings),typeé a única opção. Tentar forçarinterfaceaqui leva a workarounds que perdem a expressividade do mapped.
Em entrevista
“For component props, my rule is:
interfacewhen consumers might want to extend the type — declaration merging works on interfaces, not types.typewhen I need unions, intersections, mapped types, or conditional types — interfaces can’t do those. In practice, in product code I default totypebecause it’s simpler and covers most cases. In library code, I lean towardinterfacefor public APIs because users can extend them. For partial defaults or configs, I usesatisfiesinstead of:annotation, becausesatisfiesvalidates shape without widening literal types.”
Vocabulário-chave: declaration merging, type alias, literal type widening, discriminated union.
Pergunta típica de senior interview: “When would you choose interface over type for component props?” — resposta defensiva: when consumers (or other modules) need to extend the props — declaration merging and extends are interface-only features. For closed types, especially anything involving unions, mapped types, or conditional types, type is the only option that works.
Veja também
- 02 - Inferir vs anotar - quando deixar o TS trabalhar
- 09 - Tipando reducers e state machines — discriminated unions em ação
- 12 - Generic components
- TypeScript — seção “Interfaces e Type aliases”