Una interface declara una serie de métodos y propiedades que deben ser implementados luego por una o más clases.
Las interfaces vienen a suplir la imposibilidad de herencia múltiple.
Por ejemplo podemos tener dos clases que representen un avión y un helicóptero. Luego plantear una interface con un método llamado volar. Las dos clases pueden implementar dicha interface y codificar el método volar (los algoritmos seguramente sean distintos pero el comportamiento de volar es común tanto a un avión como un helicóptero)
La sintaxis en TypeScript para declarar una interface es:
interface [nombre de la interface] {
[declaración de propiedades]
[declaración de métodos]
}
Definir una interface llamada Punto que declare un método llamado imprimir. Luego declarar dos clases que la implementen.
interface Punto {
imprimir(): void;
}
class PuntoPlano implements Punto{
constructor(private x:number, private y:number) {}
imprimir() {
console.log(`Punto en el plano: (${this.x},${this.y})`);
}
}
class PuntoEspacio implements Punto{
constructor(private x:number, private y:number, private z:number) {}
imprimir() {
console.log(`Punto en el espacio: (${this.x},${this.y},${this.z})`);
}
}
let puntoPlano1: PuntoPlano;
puntoPlano1 = new PuntoPlano(10, 4);
puntoPlano1.imprimir();
let puntoEspacio1: PuntoEspacio;
puntoEspacio1 = new PuntoEspacio(20, 50, 60);
puntoEspacio1.imprimir();
Para declarar una interface en TypeScript utilizamos la palabra clave interface y seguidamente su nombre. Luego entre llaves indicamos todas las cabeceras de métodos y propiedades. En nuestro ejemplo declaramos la interface Punto e indicamos que quien la implemente debe definir un método llamado imprimir sin parámetros y que no retorna nada:
interface Punto {
imprimir(): void;
}
Por otro lado declaramos dos clases llamados PuntoPlano con dos propiedades y PuntoEspacio con tres propiedades, además indicamos que dichas clases implementarán la interface Punto:
class PuntoPlano implements Punto{
constructor(private x:number, private y:number) {}
imprimir() {
console.log(`Punto en el plano: (${this.x},${this.y})`);
}
}
class PuntoEspacio implements Punto{
constructor(private x:number, private y:number, private z:number) {}
imprimir() {
console.log(`Punto en el espacio: (${this.x},${this.y},${this.z})`);
}
}
La sintaxis para indicar que una clase implementa una interface requiere disponer la palabra clave implements y en forma seguida el o los nombres de interfaces a implementar. Si una clase hereda de otra también puede implementar una o más interfaces.
El método imprimir en cada clase se implementa en forma distinta, en uno se imprimen 3 propiedades y en la otra se imprimen 2 propiedades.
Luego definimos un objeto de la clase PuntoPlano y otro de tipo PuntoEspacio:
let puntoPlano1: PuntoPlano; puntoPlano1 = new PuntoPlano(10, 4); puntoPlano1.imprimir(); let puntoEspacio1: PuntoEspacio; puntoEspacio1 = new PuntoEspacio(20, 50, 60); puntoEspacio1.imprimir();
Si una clase indica que implementa una interfaz y luego no se la codifica, se genera un error en tiempo de compilación informándonos de tal situación (inclusive el editor Visual Studio Code detecta dicho error antes de compilar):
prueba.ts(5,7): error TS2420: Class 'PuntoPlano' incorrectly implements interface 'Punto'. Property 'imprimir' is missing in type 'PuntoPlano'. prueba.ts(20,13): error TS2339: Property 'imprimir' does not exist on type 'PuntoPlano'.
Este error se produce si codificamos la clase sin implementar el método imprimir:
class PuntoPlano implements Punto{
constructor(private x:number, private y:number) {}
}
Se tiene la siguiente interface:
interface Figura {
superficie: number;
perimetro: number;
calcularSuperficie(): number;
calcularPerimetro(): number;
}
Declar dos clases que representen un Cuadrado y un Rectángulo. Implementar la interface Figura en ambas clases.
interface Figura {
superficie: number;
perimetro: number;
calcularSuperficie(): number;
calcularPerimetro(): number;
}
class Cuadrado implements Figura {
superficie: number;
perimetro: number;
constructor(private lado:number) {
this.superficie = this.calcularSuperficie();
this.perimetro = this.calcularPerimetro();
}
calcularSuperficie(): number {
return this.lado * this.lado;
}
calcularPerimetro(): number {
return this.lado * 4;
}
}
class Rectangulo implements Figura {
superficie: number;
perimetro: number;
constructor(private ladoMayor:number, private ladoMenor:number) {
this.superficie = this.calcularSuperficie();
this.perimetro = this.calcularPerimetro();
}
calcularSuperficie(): number {
return this.ladoMayor * this.ladoMenor;
}
calcularPerimetro(): number {
return (this.ladoMayor * 2) + (this.ladoMenor * 2);
}
}
let cuadrado1: Cuadrado;
cuadrado1 = new Cuadrado(10);
console.log(`Perimetro del cuadrado : ${cuadrado1.calcularPerimetro()}`);
console.log(`Superficie del cuadrado : ${cuadrado1.calcularSuperficie()}`);
let rectangulo1: Rectangulo;
rectangulo1 = new Rectangulo(10, 5);
console.log(`Perimetro del rectangulo : ${rectangulo1.calcularPerimetro()}`);
console.log(`Superficie del cuadrado : ${rectangulo1.calcularSuperficie()}`);
En este problema la interface Figura tiene dos métodos que deben ser implementados por las clases y dos propiedades que también deben definirlos:
interface Figura {
superficie: number;
perimetro: number;
calcularSuperficie(): number;
calcularPerimetro(): number;
}
La clase Cuadrado indica que implementa la interface Figura, esto hace necesario que se implementen los métodos calcularSuperficie y calcularPerimetro, y las dos propiedades:
class Cuadrado implements Figura {
superficie: number;
perimetro: number;
constructor(private lado:number) {
this.superficie = this.calcularSuperficie();
this.perimetro = this.calcularPerimetro();
}
calcularSuperficie(): number {
return this.lado * this.lado;
}
calcularPerimetro(): number {
return this.lado * 4;
}
}
La clase Cuadrado tiene una propiedad llamada lado que la recibe el constructor.
De forma similar la clase Rectangulo implementa la interface Figura:
class Rectangulo implements Figura {
superficie: number;
perimetro: number;
constructor(private ladoMayor:number, private ladoMenor:number) {
this.superficie = this.calcularSuperficie();
this.perimetro = this.calcularPerimetro();
}
calcularSuperficie(): number {
return this.ladoMayor * this.ladoMenor;
}
calcularPerimetro(): number {
return (this.ladoMayor * 2) + (this.ladoMenor * 2);
}
}
Finalmente definimos un objeto de la clase Cuadrado y otro de la clase Rectangulo, luego llamamos a los métodos calcularPerimetro y calcularSuperficie para cada objeto:
let cuadrado1: Cuadrado;
cuadrado1 = new Cuadrado(10);
console.log(`Perimetro del cuadrado : ${cuadrado1.calcularPerimetro()}`);
console.log(`Superficie del cuadrado : ${cuadrado1.calcularSuperficie()}`);
let rectangulo1: Rectangulo;
rectangulo1 = new Rectangulo(10, 5);
console.log(`Perimetro del rectangulo : ${rectangulo1.calcularPerimetro()}`);
console.log(`Superficie del cuadrado : ${rectangulo1.calcularSuperficie()}`);
Las interfaces exige que una clase siga las especificaciones de la misma y se implementen algoritmos más robustos. En nuestro ejemplo tanto la clase Rectangulo como Cuadrado tienen una forma similar de trabajar gracias a que implementan la interfaz Figura.
Un método o función puede recibir como parámetro una interface. Luego le podemos pasar objetos de distintas clases que implementan dicha interface:
interface Figura {
superficie: number;
perimetro: number;
calcularSuperficie(): number;
calcularPerimetro(): number;
}
class Cuadrado implements Figura {
superficie: number;
perimetro: number;
constructor(private lado:number) {
this.superficie = this.calcularSuperficie();
this.perimetro = this.calcularPerimetro();
}
calcularSuperficie(): number {
return this.lado * this.lado;
}
calcularPerimetro(): number {
return this.lado * 4;
}
}
class Rectangulo implements Figura {
superficie: number;
perimetro: number;
constructor(private ladoMayor:number, private ladoMenor:number) {
this.superficie = this.calcularSuperficie();
this.perimetro = this.calcularPerimetro();
}
calcularSuperficie(): number {
return this.ladoMayor * this.ladoMenor;
}
calcularPerimetro(): number {
return (this.ladoMayor * 2) + (this.ladoMenor * 2);
}
}
function imprimir(fig: Figura) {
console.log(`Perimetro: ${fig.calcularPerimetro()}`);
console.log(`Superficie: ${fig.calcularSuperficie()}`);
}
let cuadrado1: Cuadrado;
cuadrado1 = new Cuadrado(10);
console.log('Datos del cuadrado');
imprimir(cuadrado1);
let rectangulo1: Rectangulo;
rectangulo1 = new Rectangulo(10, 5);
console.log('Datos del rectángulo');
imprimir(rectangulo1);
La función imprimir recibe como parámetro fig que es de tipo Figura:
function imprimir(fig: Figura) {
console.log(`Perimetro: ${fig.calcularPerimetro()}`);
console.log(`Superficie: ${fig.calcularSuperficie()}`);
}
Podemos luego llamar a la función imprimir pasando tanto objetos de la clase Cuadrado como Rectangulo:
imprimir(cuadrado1); imprimir(rectangulo1);
Es importante notar que solo podemos acceder a los métodos y propiedades definidos en la interface y no a propiedades y métodos propios de cada clase.
TypeScript permite crear objetos a partir de una interfaz. La sintaxis para dicha creación es:
interface Punto {
x: number;
y: number;
}
let punto1: Punto;
punto1 = {x:10, y:20};
console.log(punto1);
No podemos utilizar el operador new para la creación del objeto.
Podemos definir la variable e inmediatamente iniciarla:
let punto1: Punto = {x:10, y:20};
Una interface puede definir propiedades opcionales que luego la clase que la implementa puede o no definirlas. Se utiliza la misma sintaxis de los parámetros opcionales, es decir se le agrega el caracter '?' al final del nombre de la propiedad.
interface Punto {
x: number;
y: number;
z?: number;
}
let puntoPlano: Punto = {x:10, y:20};
console.log(puntoPlano);
let puntoEspacio: Punto = {x:10, y:20, z:70};
console.log(puntoEspacio);
Como vemos el objeto 'puntoPlano' solo implementa las propiedades 'x' e 'y'.
Se produce un error en tiempo de compilación si no implementamos todas las propiedades obligatorias, por ejemplo:
let puntoPlano: Punto = {x:10};
Esta línea genera un error ya que solo se define la propiedad 'x' y falta definir la propiedad 'y'.
TypeScript permite que una interface herede de otra:
interface Punto {
x: number;
y: number;
}
interface Punto3D extends Punto {
z: number;
}
let punto1: Punto = {x:10, y:20};
let punto2: Punto3D = {x:23, y:13, z:12};
console.log(punto1);
console.log(punto2);