Uma interface dá nome ao formato de um objeto: as propriedades que ele precisa ter e o tipo de cada uma. Depois de declarada, você usa o nome como tipo, e o compilador verifica contra ela todo objeto que você passa, retorna ou atribui.
A última chamada gera o erro de compilação TS2741. As interfaces são apagadas quando o código é compilado: a saída JavaScript não tem nenhum vestígio de User, e nada verifica o formato em tempo de execução.
Declarando uma interface
A sintaxe é a palavra-chave interface, um nome (em PascalCase, por convenção) e um corpo listando os membros. Os membros podem ser separados por ponto e vírgula, vírgula ou só quebra de linha; ponto e vírgula é o estilo mais comum.
interface Product {
sku: string; // required property
price: number;
tags: string[]; // array property
dimensions: { // nested object type
width: number;
height: number;
};
discount?: number; // optional property
readonly createdAt: Date; // cannot be reassigned
label(): string; // method
}
Uma interface é um tipo, não um valor. Ela não pode ser instanciada com new, não tem valores padrão, e obj instanceof Product gera o erro TS2693 ('Product' only refers to a type, but is being used as a value here). Para verificar um formato em tempo de execução, escreva um type guard.
Tipagem estrutural e verificação de propriedades extras
O TypeScript compara formatos, não nomes. Qualquer objeto com as propriedades exigidas se encaixa na interface, tenha ele sido declarado com ela ou não. Propriedades extras são aceitas, com uma exceção: um objeto literal escrito diretamente onde a interface é esperada passa por uma verificação de propriedades extras, porque uma chave desconhecida ali quase sempre é erro de digitação.
Esse erro (TS2353) é o que pega { id: 1, name: "a", emial: "x" } para um User: o compilador até sugere Did you mean to write 'email'? (TS2561).
Propriedades opcionais e readonly
Um ? depois do nome torna a propriedade opcional: o objeto pode omiti-la, e lê-la dá T | undefined. readonly proíbe reatribuir a propriedade depois que o objeto é criado.
Dois limites aparecem aqui. Primeiro, readonly vale só em tempo de compilação: as duas linhas marcadas com @ts-expect-error rodam mesmo assim quando você clica em Run, e funcionam, e as últimas linhas mudam apiUrl por meio de uma referência tipada sem readonly. Segundo, ele é raso: readonly hosts: string[] impediria reatribuir hosts, mas ainda permitiria hosts.push(...), e é por isso que o próprio array é tipado como readonly string[]. Ele documenta e impõe uma intenção no código tipado; não congela nada. O utility type Readonly<T> torna readonly de uma vez todas as propriedades de uma interface existente.
Métodos e propriedades de função
Um método pode ser escrito como method signature, name(params): ReturnType, ou como uma propriedade que guarda uma função, name: (params) => ReturnType. Quem chama usa os dois do mesmo jeito.
A diferença é sutil: com strictFunctionTypes (parte do strict), os parâmetros de propriedades tipadas como função são verificados de forma estrita, enquanto method signatures são verificadas de forma mais frouxa (bivariante), então a forma de propriedade pega alguns erros a mais. A sintaxe de método é mais curta e o estilo mais comum; as duas servem.
Uma interface também pode descrever algo que pode ser chamado ou construído, usando uma call signature ou uma construct signature:
interface Formatter {
(value: number): string; // call signature: the object is a function
locale: string; // and it also has a property
}
interface PointConstructor {
new (x: number, y: number): { x: number; y: number }; // construct signature
}
Index signatures
Quando os nomes das propriedades não são conhecidos de antemão, uma index signature descreve todas de uma vez: [key: string]: T significa "qualquer chave string, cada uma guardando um T".
As últimas linhas mostram o porém: ler uma chave que não existe tem o tipo number, e não number | undefined. A opção do compilador noUncheckedIndexedAccess acrescenta | undefined a toda leitura desse tipo.
Propriedades com nome podem ficar ao lado de uma index signature, mas precisam caber nela. interface Dict { [key: string]: number; name: string } gera o erro TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. Amplie o tipo do índice ([key: string]: number | string) ou mova a parte dinâmica para uma propriedade própria. Para mapas simples de chave e valor, Record<string, number> diz o mesmo em uma linha.
Estendendo uma interface
extends cria uma nova interface a partir de uma ou mais existentes. A filha tem todos os membros dos pais mais os próprios:
interface Animal {
name: string;
}
interface Pet extends Animal {
owner: string;
}
interface Trained {
commands: string[];
}
interface ServiceDog extends Pet, Trained {
certifiedUntil: Date;
}
// ServiceDog requires: name, owner, commands, certifiedUntil
Uma filha só pode redeclarar uma propriedade do pai com um tipo compatível (mais estreito), como kind: "dog" onde o pai diz kind: string. As regras, e como estender type aliases, estão na página sobre extends.
Implementando uma interface em uma classe
class X implements Shape pede ao compilador para verificar que a classe tem tudo o que a interface exige. Um membro faltando é um erro na declaração da classe:
index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
Property 'area' is missing in type 'Circle' but required in type 'Shape'.
Com area() adicionado, várias classes e até um objeto simples podem ser usados como Shape:
implements é só uma verificação. Ele não acrescenta membros à classe e não tipa os parâmetros dos métodos da classe para você: greet(name) {} dentro de uma classe que implementa greet(name: string): string continua gerando o erro TS7006, Parameter 'name' implicitly has an 'any' type. Uma classe pode implementar várias interfaces: class A implements B, C.
Declaration merging
Declarar duas vezes uma interface com o mesmo nome no mesmo escopo junta as duas em uma. Isso é algo que type aliases não conseguem fazer (um segundo type com o mesmo nome é erro de identificador duplicado).
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };
Em código de aplicação isso raramente é o que você quer, e uma junção acidental pode confundir. O uso real é acrescentar membros a tipos que não são seus: as opções de uma biblioteca, ou um global como Window. De dentro de um módulo, envolva a declaração em declare global:
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
export {};
Depois disso, window.analytics.track("signup") passa na verificação de tipos em todo o projeto. Os pacotes de definições de tipo dependem do mesmo mecanismo; veja arquivos de declaração.
Valores padrão para propriedades de interface
Uma interface não pode guardar valores padrão, porque descreve tipos e é apagada em tempo de execução. size?: "sm" | "md" = "md" gera o erro TS1246, An interface property cannot have an initializer. Torne a propriedade opcional e preencha o padrão onde o objeto é usado:
Os valores padrão na desestruturação são a escolha mais segura: eles valem sempre que o valor é undefined, inclusive um size: undefined explícito. A versão com spread copia esse undefined explícito por cima do padrão, e o resultado continua tipado como se size estivesse sempre definido. Se você precisa dessa garantia nos tipos, ligue o exactOptionalPropertyTypes, que torna size: undefined um erro de compilação para um size?: ... opcional. Uma classe com campos inicializados é a outra opção quando o objeto também precisa de comportamento.
Interfaces genéricas
Uma interface pode receber type parameters, o que faz uma única declaração funcionar para vários tipos de conteúdo:
interface ApiResponse<T> {
ok: boolean;
data: T;
error?: string;
}
interface Page<T> {
items: T[];
nextCursor?: string;
}
interface User {
id: number;
name: string;
}
const res: ApiResponse<Page<User>> = {
ok: true,
data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};
ApiResponse<Page<User>> se lê como "uma resposta cujo data é uma página de usuários". A biblioteca padrão está cheia delas: Array<T>, Promise<T> e Map<K, V> são todas interfaces genéricas.
Interface vs type alias
Um alias type pode descrever o mesmo formato de objeto, e para tipos de objeto simples os dois são intercambiáveis. Só uma interface pode ser mesclada; só um type alias pode dar nome a uma union, uma tupla ou um mapped ou conditional type. A regra prática do handbook do TypeScript é usar interface até precisar de um recurso que só o type tem. A página sobre interface vs type tem a comparação completa, incluindo a diferença com Record<string, ...> que surpreende a maioria das pessoas.
Perguntas frequentes
O que é uma interface no TypeScript?
Uma interface é uma descrição com nome do formato de um objeto: os nomes das propriedades, os tipos delas, quais são opcionais ou readonly e os métodos. O compilador verifica que os valores usados como aquela interface têm esse formato. Interfaces existem só em tempo de compilação; elas não geram JavaScript.
Como definir um valor padrão em uma interface do TypeScript?
Não dá: uma interface descreve tipos, não valores, então size: "md" = ... não é sintaxe válida. Marque a propriedade como opcional (size?: "sm" | "md") e aplique o padrão onde o objeto é usado, normalmente com valores padrão na desestruturação dos parâmetros da função: function render({ size = "md" }: Options). Espalhar um objeto de padrões ({ ...DEFAULTS, ...options }) também funciona, mas um undefined explícito em options sobrescreve o padrão.
Como verificar em tempo de execução se um objeto implementa uma interface?
Não existe um jeito nativo, porque as interfaces são apagadas na compilação: obj instanceof User gera o erro TS2693 ('User' only refers to a type, but is being used as a value here). Escreva uma função type guard que verifica as propriedades, function isUser(x: unknown): x is User { ... }, ou valide com uma biblioteca de schema.
Uma interface pode estender várias interfaces?
Sim. Liste-as depois de extends, separadas por vírgulas: interface ServiceDog extends Pet, Trained { ... }. A nova interface tem todos os membros de cada pai mais os próprios. Se dois pais declaram a mesma propriedade com tipos incompatíveis, a declaração é um erro.
Qual é a diferença entre uma interface e uma classe no TypeScript?
Uma classe existe em tempo de execução: tem um construtor, implementações de métodos, e new cria objetos a partir dela. Uma interface só descreve um formato para o compilador e é apagada da saída JavaScript. Uma classe pode declarar implements SomeInterface para que o compilador verifique se ela corresponde, e qualquer objeto simples com o formato certo também se encaixa na interface.