Type vs interface
Sim, é mais um artigo sobre type vs interface, quando usar cada um e etc, mas eu te prometo que esse vai ser diferente de todos que você já viu, exploraremos tópicos básicos (que muitos já sabem) e até os mais avançados (que poucos sabem). Também iremos ir um pouco mais longe e voltar no tempo, assim entenderemos que essa dúvida nem sempre existiu, por pelo menos dois motivos.
O começo de tudo
Um ponto que sempre é importante ao olhar pro typescript é analisar a sua história, muitas pessoas apenas usam features e pensam que aquilo sempre existiu, mas até mesmo as coisas que mais fazem sentido e parecem mais óbvias não existiam até um tempo atrás, e o ponto é justamente esse, a keyword "type" para declaração de tipos foi lançada apenas na versão 1.4 do typescript (6 versões após a primeira versão).
Enquanto isso, interfaces sempre existiram. Você provavelmente sabe que interfaces só podem definir objetos (se não sabe, não tem problema, discorreremos sobre logo logo), sendo assim, como definíamos strings literais, tipos condicionais e etc? Simples, isso nem existia ainda, a criação da keyword type possibilitou a adição de outras várias features.
Esse é o primeiro motivo da dúvida não existir antigamente. E o segundo motivo? Bom, acontece que a ideia era que types fossem usados para definir vários tipos diferentes como tuplas, unions, funções, arrays, e etc, mas NÃO objetos literais (assim como fazemos em interfaces), na primeira implementação da keyword type, usar type para objetos literais geraria uma mensagem de erro.
type User = {
name: string;
age: number;
};
// Error: Aliased type cannot be an object type literal.
// Use an interface declaration instead.
Dessa forma seria tranquilo decidir qual usar ou não, precisa definir objetos? Use interface, pro resto, use type, a ideia por trás disso era justamente evitar confusão, mas isso acabou mudando.
Após investigar, não consegui reproduzir a mensagem de erro acima, então presumo que antes da versão 1.4 ser oficialmente lançada, isso foi removido.
Aliás, aqui está a pull request da implementação da keyword type, bem como a pull request que permite que types sejam usados para definir objetos literais, e também a discussão desse tema.
Show, entendemos a história por trás disso tudo, mas e agora? Nos sobra uma dúvida, qual usar? O próprio time do typescript documentou algumas diferenças entre type vs interface, mas com certeza não todas elas, agora finalmente entraremos no assunto, explicarei as diferenças, começaremos com as diferenças mais básicas e iremos até as mais avançadas.
Interfaces só podem definir objetos, types podem definir qualquer tipo
Essa é uma das principais diferenças, interfaces servem exclusivamente pra declarar objetos, e objetos podem ter métodos e propriedades, já types podem fazer o mesmo, mas também podem definir qualquer outro tipo, como arrays, tuplas, unions, funções, tipos literais, mapped types, e até tipos primitivos como string, number, boolean, date, etc.
Um ponto que vale a ressalva é que interfaces não podem estender unions, mesma que seja uma union de objetos, afinal, o que teríamos como resultado final seria uma union.
type Admin = {
name: string;
role: 'admin';
};
type Moderator = {
name: string;
role: 'moderator';
};
type User = Admin | Moderator
interface Entity extends User {};
// An interface can only extend an object type or intersection of
// object types with statically known members.
Outro ponto de atenção é que talvez você tenha visto em sites como stackoverflow ou em artigos que apenas interfaces e classes poderiam ser usadas como contrato de uma classe, usar type geraria um erro.
type UsersRepository = {
create: (name: string) => string;
};
class Users implements UsersRepository {
create(name: string) {
return `alves says hi, ${name}`;
};
};
// Error: A class may only implement another
// class or interface.
Mas isso é parcialmente verdade, até a versão 2.1 do typescript usar type realmente geraria um erro, nas versões seguintes ambas as keywords type e interface servem pra definir contratos e serem utilizadas com implements.
Hover em type vs em interface
A primeira diferença visual que podemos notar é que o hover em interface e o hover em type são diferentes, enquanto o hover na interface mostra apenas o nome da interface, em type é mostrado o tipo definido e também o seu nome.


Criando um tipo usando interface é como se estivéssemos criando um tipo novo. Por exemplo, se algo é do tipo String (com letra maiúscula, estamos falando do constructor), o que você verá no hover é apenas o nome String (até mesmo porque String é implementado usando interface) e não todos os métodos e propriedades que String tem.
Já usando type, é como se estivéssemos criando apenas um alias, pra um tipo que pode ser anônimo ou não.
Uma outra pequena diferença visual é como interfaces e types se comportam em mensagens de erro, você pode conferir isso nesse playground.
Interfaces podem ser redeclaradas, types não
Interfaces podem ser declaradas múltiplas vezes e o TypeScript vai automaticamente mesclar essas declarações, como se fosse uma única interface. Esse processo é chamado de 'declaration merging'.
interface User {
name: string;
};
interface User {
age: number;
};
const user: User = {
name: 'alves',
age: 22
};
Já usando type, redeclarar irá gerar um erro, isso ocorre porque o type é tratado como um alias para um tipo específico (como mencionado no tópico acima) e, uma vez definido, não pode ser modificado ou estendido através de uma nova declaração.
type User = {
name: string;
} // Duplicate identifier 'User'.
type User = {
age: number;
} // Duplicate identifier 'User'.
const user: User = {
name: 'alves',
age: 22 // Object literal may only specify known properties,
// and 'age' does not exist in type 'User'.
};
Esse comportamento de 'declaration merging' pode ou não ser um problema, a depender do seu cenário. Mas geralmente definir um tipo novamente com o mesmo nome tende a ser um erro não proposital, falaremos mais sobre em breve.
Interfaces não podem ser utilizadas para criar tipos derivados
Uma das coisas que sempre digo é que quanto menos tipos manualmente você escrever, melhor. Pra isso, constantemente uso e recomendo tipos derivados, que geralmente vêm de um valor já existente em runtime, através do operador typeof, essa abordagem é menos suscetível a erros, reduz a quantidade de código escrito e deixa o código mais contundente (tema pra um próximo artigo 👀).
const apiConfig = {
endpoints: {
users: "/api/users",
products: "/api/products",
orders: "/api/orders"
},
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
timeout: 5000,
retries: 3
} as const;
type ApiEndpoints = typeof apiConfig.endpoints;
type EndpointKeys = keyof ApiEndpoints;
O problema é que outra limitação da keyword interface é que ela não pode ser usada para representar tipos derivados, mesmo que esse tipo seja um objeto.
const apiConfig = {
// ...
} as const;
interface ApiEndpoints extends (typeof apiConfig.endpoints) {};
// An interface can only extend an identifier/qualified-name with
// optional type arguments.
Há uma issue aberta no repositório do typescript que apesar de não citar exatamente o operador typeof como no exemplo, segue um princípio parecido, esperançosamente algum dia expressões como essa serão permitidas em interfaces, mas atualmente, não funcionam.
E um disclaimer importante e talvez óbvio: você pode usar normalmente typeof em propriedades de interfaces, só não na definição delas.
interface ApiEndpoints {
values: typeof apiConfig.endpoints;
};
Interfaces se comportam de maneira diferente de types ao compor objetos
Podemos compor objetos de formas diferentes, usando a keyword type, precisamos utilizar o operador '&', que cria uma interseção, já com interface, utilizamos a keyword 'extends'.
Apesar de propósitos parecidos, existe uma diferença entre '&' e 'extends', ao compor objetos, uma interseção cria um novo objeto, sem fazer nenhuma comparação de compatibilidade de tipos das propriedades, em casos em que dois ou mais objetos possuem uma propriedade com o mesmo nome, os seus tipos serão interseccionados, e isso frequentemente resulta em never.
type Admin = {
name: string;
document: string;
};
type Moderator = {
age: number;
document: number;
};
type User = Admin & Moderator
const user: User = {
name: 'alves',
age: 22,
document: '1234' // Type 'string' is not assignable to type 'never'.
// since string & number is equals to never
};
Já com interfaces, as propriedades com os mesmos nomes precisam ter os mesmos tipos. Na maioria dos casos, esse comportamento é especialmente útil, mas depende do seu cenário.
interface Admin {
name: string;
document: string;
};
interface Moderator {
document: number;
age: number;
};
interface User extends Admin, Moderator {}
// Interface 'User' cannot simultaneously extend types 'Admin'
// and 'Moderator'.
// Named property 'document' of types 'Admin' and 'Moderator'
// are not identical.
Types têm assinatura implícita de índice
Um comportamento sutil que difere entre type e interface é que types têm assinatura implícita de índice, e interfaces não, pergunta: há algum erro no código abaixo?
type User = {
name: string;
};
const user: User = {
name: 'alves'
};
const test: { [key: string]: string } = user;
Se disse que 'não', então você acertou! Faz sentido, a variável 'test' deve ser um objeto que tem propriedades e valores do tipo string, e a variável 'user' é atribuível a isso.
Mas e se mudássemos a declaração de 'User' para usarmos interface ao invés de type?
interface User {
name: string;
}
const user: User = {
name: 'alves'
};
const test: { [key: string]: string} = user;
// Type 'User' is not assignable to type '{ [key: string]: string; }'.
// Index signature for type 'string' is missing in type 'User'.
Obteríamos um erro, estranho, né? Lembra que mencionei que interfaces podem ser redeclaradas? É esse o causador desse erro, o TypeScript sabe que essa interface pode ser redeclarada, e por isso não necessariamente ela só terá propriedades e valores do tipo string, logo, nenhuma assinatura de índice é definida implicitamente.
Podemos resolver isso usando uma assinatura explícita de índice, mas isso faria com que a interface também aceitasse outras propriedades além da propriedade 'name'.
interface User {
name: string;
[key: string]: string;
}
const user: User = {
name: 'alves'
};
const test: { [key: string]: string} = user; // no errors
// but now this is ok
const userTwo: User = {
name: 'alves',
other: 'random',
};
Interfaces têm melhor performance do que types ao compor vários objetos
O TypeScript foi projetado para que as interfaces sejam verificadas de maneira mais otimizada, é preferível que usemos interface para compor vários objetos ao invés de type, por questões de performance, já que as relações entre interfaces são cacheadas, inclusive essa performance melhor em interfaces é algo documentado pelo próprio time do typescript, e você pode ver mais sobre isso na wiki do typescript.
interface Admin {
// a object with a lot of properties
};
interface Moderator {
// a object with a lot of properties
};
interface Owner {
// a object with a lot of properties
}
interface User extends Admin, Moderator, Owner {}
Em resumo, quando temos uma interseção com mais de 2 objetos, essa diferença de performance começa a ser mais significante, e pra isso ser um problema é algo que precisa acontecer várias vezes, não precisa ir refatorar suas interseções para usar interfaces (ainda), mas faz bem levar isso em conta ao escolher entre um ou outro.
Interfaces têm this
Chegamos na minha parte favorita desse artigo, e na mais complexa/avançada, aquela que não te contam quando você pergunta a diferença entre type vs interface, citarei o termo 'Higher Kinded Types', mas não explicarei com profundidade o que são, esse é um tema que merece um artigo especial só pra ele.
Outra diferença importante que pode fazer toda a diferença na escolha entre interfaces e types é que interfaces podem usar o tipo 'this' na definição de retorno de funções/métodos e também em suas propriedades, enquanto types não têm essa capacidade.
type UsersRepository = {
createUser: () => this
} // A 'this' type is available only in a non-static
// member of a class or interface.
O caso de uso mais comum é a utilização do pattern fluent api, que é uma maneira de deixar seu código mais legível e fácil de entender, encadeando métodos de uma forma que pareça uma frase ou uma série de instruções.
interface Counter {
count: number;
increment(): this;
reset(): this;
}
class MyCounter implements Counter {
count = 0;
increment() {
this.count++;
return this;
}
reset() {
this.count = 0;
return this;
}
}
const counter = new MyCounter();
counter.increment().increment().reset();
Com 'this' também podemos criar mixins, que são classes/objetos que contêm métodos para outras classes/objetos, permitindo a adição de funcionalidade sem usar herança.
interface Timestamped {
timestamp: Date;
setTimestamp(date: Date): this;
}
interface Named {
name: string;
setName(name: string): this;
}
class User implements Timestamped, Named {
timestamp = new Date();
name = "alves";
setTimestamp(date: Date) {
this.timestamp = date;
return this;
}
setName(name: string) {
this.name = name;
return this;
}
}
new User().setName("John").setTimestamp(new Date());
Também podemos utilizar o 'this' em propriedades da interface, observemos o comportamento abaixo:
interface User {
name: string
fullName: this['name']
}
Isso faz com que a propriedade 'fullName' seja do tipo string, assim como foi definido em name.
O que é interessante mas inútil, certo? Daria pra manualmente tipar a propriedade 'fullName' como string, ou até mesmo referenciar a própria interface, qual a vantagem do 'this'?
interface User {
name: string
fullName: string
}
// or
interface User {
name: string
fullName: User['name'] // string
}
Antes, precisamos relembrar um comportamento existente com o tipo unknown: ao ser interseccionado com algum outro tipo, o resultado é sempre o tipo interseccionado, isto é:
type Str = unknown & string // string
type Nbr = unknown & number // number
type Bool = unknown & boolean // boolean
type Any = unknown & any // any
type Func = unknown & (() => void) // () => void
type Union = unknown & 'a' | 'b' // 'a' | 'b'
A grande vantagem vem ao combinar o comportamento do 'this' e unknown + extends, o 'this' sempre referencia o último tipo na cadeia de extensões, e qualquer tipo interseccionado com unknown se resolve nele mesmo, permitindo a construção disso:
interface User {
name: string;
age: unknown;
values: this['age']; // unknown
}
interface Admin extends User {
age: number;
}
const admin: Admin = {
name: 'alves',
age: 22,
values: 22
}
A propriedade 'values' é do tipo unknown na interface 'User', já na interface 'Admin' ela passa a ser number, já que a propriedade 'age' se tornou number.
E é esse comportamento que bibliotecas aproveitam pra construir Higher Kinded Types, Embora TypeScript não suporte diretamente Higher Kinded Types (HKTs), que são comuns em linguagens como Haskell e Scala, é assim que podemos fazer para simular alguns desses comportamentos.
Higher Kinded Types (HKTs) são tipos que operam sobre outros tipos. Enquanto um tipo normal pode ser string ou number, e um tipo genérico pode ser algo como Array<T> ou Promise<T>, um higher kinded type seria algo como F<T> onde F é um tipo de tipo, ou seja, é a abstração da abstração da abstração 😅.
Os tipos podem ser categorizados por ordem/nível:
Tipos de primeira ordem: são tipos simples como string, number, boolean, etc.
Tipos parametrizados (ou genéricos): são tipos que recebem outros tipos como parâmetros, como Array<T>, Promise<T>, Map<K, V>, etc.
Higher kinded types (HKTs): são 'tipos de tipos' ou 'construtores de tipos'. Eles não são tipos completos por si só, mas precisam de outros tipos para formar um tipo completo
Analogamente, isso seria:
Um tipo normal é como um valor (ex: 5 ou 'hello').
Um tipo genérico é como uma função que recebe valores f(x) = x + 1 .
Um higher kinded type é como uma função que recebe funções g(f) = f(f(x))
Por ser deveras um tema mais avançado e complexo, também por ser algo mais nichado (poucas pessoas precisarão usar isso na vida, já que o uso é mais comum em bibliotecas), e dado o tamanho desse artigo, irei parar por aqui e deixar sua curiosidade lhe guiar, caso queira se aprofundar e ver exemplos de casos de uso disso na prática, recomendo que veja o código da biblioteca effect e leia esse artigo do próprio mantenedor da biblioteca falando sobre HKTs.
Está tudo bem se você não tiver entendido esse último tópico, falha minha, já que eu ainda não consigo simplificar esse tema ao ponto de trazer um exemplo bem simplório que lhe faça entender a utilidade, usuários comuns tendem a não esbarrar nesse tipo de problema, então talvez você não deva se preocupar tanto com isso, como eu havia dito, um artigo especialmente sobre o assunto é necessário, e isso vai ficar pra uma próxima.
Usar type ou interface?
Dito tudo isso, e agora? Devo usar type ou interface?
Depende 😁, brincadeiras a parte, existem algumas abordagens que você pode adotar:
Seguir a recomendação do time do TypeScript, que é utilizar interface até que você precise de features de type.
O contrário disso, que é utilizar type até que você precise de features de interface.
E, por fim, utilizar sempre interface para definir objetos, e type para todo o resto.
Se você estiver construindo uma biblioteca, sugiro que sempre pense primeiro em interface para definir objetos, por causa do seu comportamento de declaration merging, permitindo que usuários da sua biblioteca estendam a interface, mas lembre-se que talvez seus usuários não precisem disso, vai depender da API que você está construindo.
Considerações finais
Primeiramente, muito obrigado por acompanhar até aqui, eu espero de verdade que você tenha aprendido algo novo e saia desse artigo sabendo qual deve usar ou não, particularmente, eu alterno entre a 2° e a 3° opção. Se você achou esse artigo útil, compartilhe com alguém, se você tiver alguma dúvida, pode me mandar dm no twitter ou no discord (alvseven). Se notou algo de errado, um ponto de correção, algum detalhe que faltou, por favor não hesite em me contatar.
E pra finalizar, talvez eu tenha mentido, também é possível definir funções com interfaces, mas é algo não tão idiomático e na maioria dos casos você vai encontrar definições de funções utilizando a keyword type, se você voltar no primeiro ponto do artigo, verá que eu disse que nem sempre a keyword type existiu, dessa forma, era comum definir funções com interface, vide exemplo abaixo:
interface CreateUser {
(name: string): { id: string; name: string;}
}
const createUser: CreateUser = (name) => {
return {
id: '1',
name,
}
}