Interfaces em TypeScript
Interfaces de Objetos
Uma interface define as propriedades e métodos que um objeto deve implementar. Em outras palavras, uma interface é a definição de um tipo de dados personalizado, mas sem implementação. Neste caso, as interfaces em TypeScript são semelhantes às interfaces nas linguagens Java e C#. As interfaces são definidas usando a palavra-chave interface
. Para começar, vamos definir uma interface simples:
interface IUser {
id: number;
name: string;
}
A interface entre chaves define duas propriedades: id
, que tem o tipo number
, e name
, que representa uma string. Agora, vamos usá-la em um programa:
let employee: IUser = {
id: 1,
name: "Tom",
};
console.log("id: ", employee.id);
console.log("name: ", employee.name);
Essencialmente, employee
é um objeto comum, exceto que ele tem o tipo IUser
. Mais precisamente, employee
implementa a interface IUser
. Essa implementação impõe algumas restrições a employee
. Por exemplo, employee
deve implementar todas as propriedades e métodos da interface IUser
, portanto, ao definir employee
, o objeto deve necessariamente incluir as propriedades id
e name
.
Os parâmetros de métodos e funções também podem representar interfaces:
interface IUser {
id: number;
name: string;
}
let employee: IUser = {
id: 1,
name: "Alice",
};
function printUser(user: IUser): void {
console.log("id: ", user.id);
console.log("name: ", user.name);
}
printUser(employee);
Neste caso, o argumento que é passado para a função deve ser um objeto ou classe que implementa a interface correspondente.
E também é possível retornar objetos de uma interface:
interface IUser {
id: number;
name: string;
}
function buildUser(userId: number, userName: string): IUser {
return { id: userId, name: userName };
}
let newUser = buildUser(2, "Bill");
console.log("id: ", newUser.id);
console.log("name: ", newUser.name);
Propriedades Opcionais e Propriedades Somente Leitura
Ao definir uma interface, podemos especificar algumas propriedades como opcionais usando o ponto de interrogação. Tais propriedades não precisam ser necessariamente implementadas:
interface IUser {
id: number;
name: string;
age?: number;
}
let employee: IUser = {
id: 1,
name: "Alice",
age: 23,
};
let manager: IUser = {
id: 2,
name: "Tom",
};
A propriedade age
está marcada como opcional, portanto, não é necessário incluí-la ao definir um objeto que implementa a interface IUser
.
Além disso, uma interface pode conter propriedades somente leitura, cujo valor não pode ser alterado. Tais propriedades são definidas com a palavra-chave readonly
:
interface Point {
readonly x: number;
readonly y: number;
}
let p: Point = { x: 10, y: 20 };
console.log(p);
// p.x = 5; // Erro - propriedade somente leitura
Definição de Métodos
Além das propriedades, as interfaces podem definir funções:
interface IUser {
id: number;
name: string;
sayWords(words: string): void;
}
let employee: IUser = {
id: 1,
name: "Alice",
sayWords: function (words: string): void {
console.log(`${this.name} diz "${words}"`);
},
};
employee.sayWords("Olá, como vai?");
interface IUser {
id: number;
name: string;
sayWords(words: string): void;
}
let employee: IUser = {
id: 1,
name: "Alice",
sayWords: function (words: string): void {
console.log(`${this.name} diz "${words}"`);
},
};
employee.sayWords("Olá, como vai?");
Novamente, um objeto que implementa a interface é obrigado a implementar a função definida na interface com o mesmo conjunto de parâmetros e o mesmo tipo de resultado. Neste caso, a função sayWords()
recebe uma string como parâmetro e não retorna nada, exibindo uma mensagem no console.
Interfaces de Classes
As interfaces podem ser implementadas não apenas por objetos, mas também por classes. Para isso, utiliza-se a palavra-chave implements
:
interface IUser {
id: number;
name: string;
getFullName(surname: string): string;
}
class User implements IUser {
id: number;
name: string;
age: number;
constructor(userId: number, userName: string, userAge: number) {
this.id = userId;
this.name = userName;
this.age = userAge;
}
getFullName(surname: string): string {
return this.name + " " + surname;
}
}
let tom = new User(1, "Tom", 23);
console.log(tom.getFullName("Simpson"));
A classe User
implementa a interface IUser
. Nesse caso, a classe User
é obrigada a definir todas as propriedades e métodos que existem em IUser
.
Além disso, o objeto tom é tanto um objeto User
quanto um objeto IUser
:
let tom: IUser = new User(1, "Tom", 23);
// ou
let tom: User = new User(1, "Tom", 23);
Extensão de Interface
O TypeScript permite adicionar novos campos e métodos a uma interface simplesmente declarando a interface com o mesmo nome e definindo nela os campos e métodos necessários. Por exemplo:
interface IUser {
id: number;
name: string;
}
interface IUser {
age: number;
}
let employee: IUser = {
id: 1,
name: "Alice",
age: 31,
};
function printUser(user: IUser): void {
console.log(`id: ${user.id} name: ${user.name} age: ${user.age}`);
}
printUser(employee);
Neste caso, a primeira definição da interface IUser
contém os campos id
e name
. A segunda definição da interface inclui a declaração do campo age
. Como resultado, um objeto ou classe que implementa essa interface deve definir todos os três campos: id
, name
e age
.
Herança de Interfaces
As interfaces, assim como as classes, podem ser herdadas:
interface IMovable {
speed: number;
move(): void;
}
interface ICar extends IMovable {
fill(): void;
}
class Car implements ICar {
speed: number;
move(): void {
console.log("O carro está se movendo a " + this.speed + " km/h");
}
fill(): void {
console.log("Abastecendo o carro com combustível");
}
}
let auto = new Car();
auto.speed = 60;
auto.fill();
auto.move();
Após a herança, a interface ICar
também terá todas as propriedades e funções definidas em IMovable
. Portanto, a classe Car
, que implementa a interface ICar
, também deve implementar as propriedades e métodos da interface IMovable
.
Interfaces de Função
Interfaces de função contêm a definição do tipo de uma função. Elas devem ser implementadas por um objeto que representa uma função desse tipo:
interface FullNameBuilder {
(name: string, surname: string): string;
}
let simpleBuilder: FullNameBuilder = function (name: string, surname: string): string {
return "Mr. " + name + " " + surname;
};
let fullName = simpleBuilder("Bob", "Simpson");
console.log(fullName); // Mr. Bob Simpson
Aqui, a interface FullNameBuilder
é definida apenas com a assinatura de uma função. Em seguida, é declarada a variável simpleBuilder
, que tem o tipo FullNameBuilder
e, portanto, deve representar uma função com essa assinatura.
Interfaces de Arrays
Interfaces de arrays descrevem objetos que podem ser acessados por índice, como em arrays:
interface StringArray {
[index: number]: string;
}
let phones: StringArray;
phones = ["iPhone 7", "HTC 10", "HP Elite x3"];
let myPhone: string = phones[0];
console.log(myPhone);
Aqui, a interface StringArray
é definida com a assinatura de um array. Essa assinatura indica que o objeto que implementa StringArray
pode ser indexado usando números (tipo number
) e deve armazenar elementos do tipo string
.
No exemplo acima, o índice é do tipo number
, mas também podemos usar o tipo string
para indexação:
interface Dictionary {
[index: string]: string;
}
let colors: Dictionary = {};
colors["red"] = "#ff0000";
colors["green"] = "#00ff00";
colors["blue"] = "#0000ff";
console.log(colors["red"]);
Interfaces Híbridas
As interfaces podem combinar diferentes estilos e podem ser aplicadas tanto à definição de um objeto quanto de uma função:
interface PersonInfo {
(name: string, surname: string): void;
fullName: string;
password: string;
authenticate(): void;
}
function personBuilder(): PersonInfo {
let person = <PersonInfo>function (name: string, surname: string): void {
person.fullName = name + " " + surname;
};
person.authenticate = function () {
console.log(person.fullName + " está se autenticando com a senha " + person.password);
};
return person;
}
let tom = personBuilder();
tom("Tom", "Simpson");
tom.password = "qwerty";
tom.authenticate();
O tipo de função definido em uma interface híbrida geralmente atua como um construtor de objetos. Neste caso, esse construtor tem o tipo (name: string, surname: string): void
.
A função que representa essa interface (neste exemplo, a função personBuilder
) implementa essa função construtora e também pode utilizar outras propriedades e métodos definidos na interface.