Types versus interfaces em 2024 - qual usar?
Essa é a pergunta de um milhão de dólares. Acho que, depois de "eu tenho que saber JavaScript pra aprender TypeScript" essa foi a pergunta que eu mais recebi até hoje: "Quando usar interfaces e quando usar tipos?"
E eu vou mais além! Por que interfaces existem se os tipos já fazem tudo que elas precisam?
Pra gente entender isso tudo vamos ter que entender o que é um tipo, e o que é uma interface e qual é a diferença entre os dois
Tipos
Tipos são os blocos de construção do TypeScript, inclusive está no nome. São as coisas mais importantes e básicas que temos na nossa linguagem.
Um tipo pode representar qualquer outro tipo no TypeScript, não apenas tipos simples e primitivos, mas objetos também.
Então a gente pode fazer coisas simples como:
type str = string
type n = number
Quanto:
type pessoa = {
nome: string
}
type filter = (predicate: string) => string
Ou seja, tipos podem representar qualquer objeto e qualquer interface no TypeScript.
Interfaces
Interfaces vem de uma abordagem orientada a objeto, oposta a uma abordagem mais funcional dos tipos. Elas existem desde a primeira versão do TypeScript e foram criadas para tornar possíveis padrões de projeto que exigem polimorfismo
Diferente de tipos, interfaces representam apenas objetos, elas não podem representar tipos simples ou primitivos.
interface pessoa {
nome: string
}
Diferenças entre tipos e interfaces
Quando estamos lidando com essas duas ferramentas que são muito parecidas, fica difícil saber quando usar um e quando usar outro. Antes de entrar nesse ponto, eu quero deixar essa seção especificamente para dizer as diferenças entre tipos e interfaces.
Interfaces não expressam tipos mapeados
Diferente de tipos, interfaces não conseguem expressar tipos mapeados, por exemplo, se a gente quiser ter um tipo Partial<T>
não podemos ter uma interface:
type partial<T> = {
[K in keyof T]?: T[K]
}
interface Pessoa {
nome: string
}
type partialPessoa = partial<Pessoa>
Tipos não expressam extensões de forma eficiente
Como a gente viu antes, tipos seguem uma abordagem mais funcional, diferente de interfaces que seguem uma abordagem mais orientada a objetos.
Isso significa que quando a gente tem um tipo que estende outro tipo, ou seja, ele é a união desses dois tipos, a gente precisa representar assim:
type idade = { idade: number }
type nome = { nome: string }
type pessoa = nome & idade // { nome: string, idade: number }
Enquanto a gente pode expressar em interfaces usando a keyword extends
:
interface Nome {
nome: string
}
interface Pessoa extends Nome {
idade: number
}
Eu, particularmente, acho interfaces mais simples de ler nesse caso. Mas tipos tem um problema quando temos que tratar com uniões e intersecções: eles não podem ser cacheados.
Toda a validação e todo o cálculo de tipos é feito em tempo real pelo compilador, enquanto interfaces poem ser cacheadas porque elas não podem ser alteradas dinamicamente, somente por algo chamado declaration merging que, coincidentemente (ou não), é o nosso próximo tópico.
Vem aprender comigo!
Quer aprender mais sobre criptografia e boas práticas com #TypeScript?
Se inscreva na Formação TS!Interfaces suportam declaration merging
Declaration merging também é chamado de "tipo aberto", enquanto interfaces são tipos abertos, todos os type aliases são considerados tipos fechados, por exemplo, não podemos criar dois tipos com o mesmo nome:
type dog = string
type dog = number // erro
Um tipo só pode existir em um único lugar, uma única vez, enquanto interfaces podem fazer o que é chamado de declaration merging, ou seja, se declaramos ela múltiplas vezes, as diferenças entre a primeira declaração e a segunda são adicionadas no mesmo tipo:
interface Pessoa {
nome: string
} // Pessoa é um objeto { nome: string }
interface Pessoa {
idade: number
} // Pessoa agora é { nome: string, idade: number }
Enquanto muitas pessoas consideram isso um problema (e usam regras de lint como o no-redeclare do ESLint), outros consideram algo ok. A verdade é que essa funcionalidade não é só ok, mas ela é necessária para o TS funcionar.
Se você observar as bibliotecas padrões do TS, você vai perceber que temos uma série de interfaces que são redeclaradas, por exemplo, na lib.es2015.promise.d.ts
, temos a declaração do que é uma promise como construtor, já no lib.es5.d.ts
temos a declaração da própria promise:
interface Promise<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}
Agora, temos outra standard library, a lib.es2018.promise.d.ts
que redeclara a mesma interface que eu acabei de mostrar, mas ela adiciona o finally
na interface:
interface Promise<T> {
/**
* Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The
* resolved value cannot be modified from the callback.
* @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected).
* @returns A Promise for the completion of the callback.
*/
finally(onfinally?: (() => void) | undefined | null): Promise<T>;
}
Isso é importante porque o time do TS não precisa manter arquivos gigantescos, a cada nova versão do ECMAScript, a galera pode criar uma nova standard library somente com as mudanças. Sem declaration merging, o TypeScript não conseguiria se manter.
Index signatures são diferentes em tipos e interfaces
Quando criamos um tipo que é um objeto, por exemplo:
type Pessoa = {
nome: string
idade: number
}
Vamos conseguir fazer algo que, na minha opinião, não deveria acontecer. Que é associar uma index signature direto para o tipo, mesmo se ele não tiver uma:
type Pessoa = {
nome: string
idade: number
}
const Joao: Pessoa = {
nome: 'João',
idade: 32
}
type RecordGenerico = Record<string, number|string>
const meuRecord: RecordGenerico = Joao
Então é como se o nosso tipo Pessoa
implicitamente tivesse isso aqui:
type Pessoa = {
nome: string
idade: number
[x: string]: string|number
}
Significando que a gente pode associar chaves que são parte de quaisquer tipos presentes no objeto do type alias.
Em interfaces isso não é permitido, e você precisa explicitamente dizer que a interface possui uma index signature, caso contrário você vai ter um erro:
interface Pessoa {
nome: string
idade: number
}
const Joao: Pessoa = {
nome: 'João',
idade: 32
}
type RecordGenerico = Record<string, number|string>
const meuRecord: RecordGenerico = Joao // erro
Mas isso funciona:
interface Pessoa {
nome: string
idade: number
[x: string]: string|number
}
const Joao: Pessoa = {
nome: 'João',
idade: 32
}
type RecordGenerico = Record<string, number|string>
const meuRecord: RecordGenerico = Joao
Obrigado! Você chegou aqui! 🎉
Se você gosta do meu conteúdo, considere assinar a minha newsletter!
Conteúdo de qualidade com a curadoria de mais de uma década como dev
Quando usar cada um
Não existe um ganho ou perda ao usar só tipos ou só interfaces, então o uso fica muito à critério de quem está utilizando esses recursos. Eu, pessoalmente, prefiro a seguinte estrutura:
- Se eu estou definindo um tipo que é um objeto, então uso interfaces
- Para todos os outros casos, use tipos
Por que?
Simplesmente porque interfaces foram pensadas com a ideia de modelar objetos dinâmicos no JavaScript, e tipos não. Então quando estamos usando interfaces o TypeScript faz uma série de otimizações para poder deixar a sua compilação um pouco mais rápida.
Além disso, interfaces podem ser estendidas de uma forma mais expressiva, e você pode trabalhar com declaration merging (com cuidado) para poder expressar tipos altamente dinâmicos, coisa que não é possível com tipos.
Mas, os tipos são muito flexíveis, então faz sentido você usar tipos em casos como:
- Criar um alias para reduzir a complexidade de um tipo mais elaborado
- Expressar utility types
- Criar genéricos que vão ser utilizados como helpers
O uso de interfaces é algo bastante pessoal. No geral, o meu conselho é que você seja consistente. Então se você está usando interfaces para objetos, não use tipos e se você está usando apenas tipos, não use interfaces.
Porém você vai ver que é bem difícil não utilizar interfaces, especialmente se você está seguindo orientação a objetos, porque é uma forma muito mais expressiva de implementar classes por exemplo.