TypeScript
Interfaces
Buscar..
Introducción
Una interfaz especifica una lista de campos y funciones que pueden esperarse en cualquier clase que implemente la interfaz. A la inversa, una clase no puede implementar una interfaz a menos que tenga todos los campos y funciones especificados en la interfaz.
El principal beneficio de usar interfaces, es que permite usar objetos de diferentes tipos de manera polimórfica. Esto se debe a que cualquier clase que implemente la interfaz tiene al menos esos campos y funciones.
Sintaxis
- interfaz InterfaceName {
- ParameterName: ParameterType;
- optionalParameterName?: parametersType;
- }
Observaciones
Interfaces vs Alias de Tipo
Las interfaces son buenas para especificar la forma de un objeto, por ejemplo, para un objeto de persona que podría especificar
interface person {
id?: number;
name: string;
age: number;
}
Sin embargo, ¿qué sucede si quiere representar, por ejemplo, la forma en que una persona se almacena en una base de datos SQL? Al ver que cada entrada de base de datos consta de una fila de forma [string, string, number]
(por lo tanto, una matriz de cadenas o números), no hay forma de que pueda representar esto como una forma de objeto, porque la fila no tiene propiedades. como tal, es sólo una matriz.
Esta es una ocasión donde los tipos son útiles. En lugar de especificar en cada función que acepte una function processRow(row: [string, string, number])
parámetro de fila function processRow(row: [string, string, number])
, puede crear un alias de tipo separado para una fila y luego usarlo en cada función:
type Row = [string, string, number];
function processRow(row: Row)
Documentación oficial de la interfaz.
https://www.typescriptlang.org/docs/handbook/interfaces.html
Añadir funciones o propiedades a una interfaz existente
Supongamos que tenemos una referencia a la definición de tipo JQuery
y queremos extenderla para que tenga funciones adicionales de un complemento que incluimos y que no tenga una definición de tipo oficial. Podemos extenderlo fácilmente declarando las funciones agregadas por el complemento en una declaración de interfaz separada con el mismo nombre JQuery
:
interface JQuery {
pluginFunctionThatDoesNothing(): void;
// create chainable function
manipulateDOM(HTMLElement): JQuery;
}
El compilador combinará todas las declaraciones con el mismo nombre en una sola - ver declaración de fusión para más detalles.
Interfaz de clase
Declare variables y métodos public
escriba en la interfaz para definir cómo otros códigos mecanografiados pueden interactuar con ella.
interface ISampleClassInterface {
sampleVariable: string;
sampleMethod(): void;
optionalVariable?: string;
}
Aquí creamos una clase que implementa la interfaz.
class SampleClass implements ISampleClassInterface {
public sampleVariable: string;
private answerToLifeTheUniverseAndEverything: number;
constructor() {
this.sampleVariable = 'string value';
this.answerToLifeTheUniverseAndEverything = 42;
}
public sampleMethod(): void {
// do nothing
}
private answer(q: any): number {
return this.answerToLifeTheUniverseAndEverything;
}
}
El ejemplo muestra cómo crear una interfaz ISampleClassInterface
y una clase SampleClass
que implements
la interfaz.
Interfaz de extensión
Supongamos que tenemos una interfaz:
interface IPerson {
name: string;
age: number;
breath(): void;
}
Y queremos crear interfaz más específico que tiene las mismas propiedades de la persona, podemos hacerlo mediante el extends
palabra clave:
interface IManager extends IPerson {
managerId: number;
managePeople(people: IPerson[]): void;
}
Además es posible extender múltiples interfaces.
Uso de interfaces para hacer cumplir tipos
Uno de los beneficios principales de Typescript es que impone tipos de datos de valores que está transmitiendo alrededor de su código para ayudar a prevenir errores.
Digamos que estás haciendo una aplicación de citas para mascotas.
Tiene esta función simple que comprueba si dos mascotas son compatibles entre sí ...
checkCompatible(petOne, petTwo) {
if (petOne.species === petTwo.species &&
Math.abs(petOne.age - petTwo.age) <= 5) {
return true;
}
}
Este es un código completamente funcional, pero sería demasiado fácil para alguien, especialmente para otras personas que trabajan en esta aplicación que no escribieron esta función, ignorar que se supone que pasan objetos con 'especies' y 'edad' propiedades Pueden equivocadamente probar checkCompatible(petOne.species, petTwo.species)
y luego checkCompatible(petOne.species, petTwo.species)
para descubrir los errores que se producen cuando la función intenta acceder a petOne.species.species o petOne.species.age.
Una forma en que podemos evitar que esto suceda es especificando las propiedades que queremos en los parámetros de mascota:
checkCompatible(petOne: {species: string, age: number}, petTwo: {species: string, age: number}) {
//...
}
En este caso, Typescript se asegurará de que todo lo que se pasa a la función tenga propiedades de 'especie' y 'edad' (está bien si tienen propiedades adicionales), pero esta es una solución poco manejable, incluso con solo dos propiedades especificadas. Con las interfaces, hay una mejor manera!
Primero definimos nuestra interfaz:
interface Pet {
species: string;
age: number;
//We can add more properties if we choose.
}
Ahora todo lo que tenemos que hacer es especificar el tipo de nuestros parámetros como nuestra nueva interfaz, como así ...
checkCompatible(petOne: Pet, petTwo: Pet) {
//...
}
... y Typescript se asegurará de que los parámetros pasados a nuestra función contengan las propiedades especificadas en la interfaz de Pet!
Interfaces genéricas
Al igual que las clases, las interfaces también pueden recibir parámetros polimórficos (también conocidos como Genéricos).
Declaración de parámetros genéricos en interfaces
interface IStatus<U> {
code: U;
}
interface IEvents<T> {
list: T[];
emit(event: T): void;
getAll(): T[];
}
Aquí, puedes ver que nuestras dos interfaces toman algunos parámetros genéricos, T y U.
Implementando interfaces genéricas
Crearemos una clase simple para implementar la interfaz de IEvents .
class State<T> implements IEvents<T> {
list: T[];
constructor() {
this.list = [];
}
emit(event: T): void {
this.list.push(event);
}
getAll(): T[] {
return this.list;
}
}
Vamos a crear algunas instancias de nuestra clase estatal .
En nuestro ejemplo, la clase State
manejará un estado genérico usando IStatus<T>
. De esta manera, la interfaz IEvent<T>
también manejará un IStatus<T>
.
const s = new State<IStatus<number>>();
// The 'code' property is expected to be a number, so:
s.emit({ code: 200 }); // works
s.emit({ code: '500' }); // type error
s.getAll().forEach(event => console.log(event.code));
Aquí nuestra clase de State
se escribe como ISatus<number>
.
const s2 = new State<IStatus<Code>>();
//We are able to emit code as the type Code
s2.emit({ code: { message: 'OK', status: 200 } });
s2.getAll().map(event => event.code).forEach(event => {
console.log(event.message);
console.log(event.status);
});
Nuestra clase de State
se escribe como IStatus<Code>
. De esta manera, podemos pasar un tipo más complejo a nuestro método de emisión.
Como puede ver, las interfaces genéricas pueden ser una herramienta muy útil para el código escrito de forma estática.
Usando interfaces para el polimorfismo
La razón principal para usar interfaces para lograr el polimorfismo y proporcionar a los desarrolladores la implementación a su manera en el futuro mediante la implementación de los métodos de la interfaz.
Supongamos que tenemos una interfaz y tres clases:
interface Connector{
doConnect(): boolean;
}
Esta es la interfaz del conector. Ahora lo implementaremos para la comunicación wifi.
export class WifiConnector implements Connector{
public doConnect(): boolean{
console.log("Connecting via wifi");
console.log("Get password");
console.log("Lease an IP for 24 hours");
console.log("Connected");
return true
}
}
Aquí hemos desarrollado nuestra clase concreta llamada WifiConnector
que tiene su propia implementación. Este es ahora el tipo de Connector
.
Ahora estamos creando nuestro System
que tiene un Connector
componente. Esto se llama inyección de dependencia.
export class System {
constructor(private connector: Connector){ #inject Connector type
connector.doConnect()
}
}
constructor(private connector: Connector)
esta línea es muy importante aquí. Connector
es una interfaz y debe tener doConnect()
. Como el Connector
es una interfaz, este System
clase tiene mucha más flexibilidad. Podemos pasar cualquier tipo que haya implementado la interfaz del Connector
. En el futuro el desarrollador consigue más flexibilidad. Por ejemplo, ahora el desarrollador desea agregar el módulo de conexión Bluetooth:
export class BluetoothConnector implements Connector{
public doConnect(): boolean{
console.log("Connecting via Bluetooth");
console.log("Pair with PIN");
console.log("Connected");
return true
}
}
Ver que Wifi y Bluetooth tienen su propia implementación. Hay manera diferente de conectarse. Sin embargo, por lo tanto, ambos han implementado Type Connector
, ahora son Type Connector
. Para que podamos pasar cualquiera de ellos a la clase System
como el parámetro constructor. Esto se llama polimorfismo. El System
clase ahora no sabe si es Bluetooth / Wifi, incluso podemos agregar otro módulo de comunicación como Inferade, Bluetooth5 y cualquier otro solo implementando la interfaz del Connector
.
Esto se llama escribir pato . Connector
tipo de Connector
ahora es dinámico, ya que doConnect()
es solo un marcador de posición y el desarrollador implementa esto como propio.
si en el constructor(private connector: WifiConnector)
donde WifiConnector
es una clase concreta, ¿qué pasará? Entonces System
clase de System
se acoplará estrechamente solo con WifiConnector nada más. Aquí la interfaz resolvió nuestro problema por el polimorfismo.
Implementación implícita y forma del objeto
TypeScript admite interfaces, pero el compilador genera JavaScript, que no lo hace. Por lo tanto, las interfaces se pierden efectivamente en el paso de compilación. Esta es la razón por la que la verificación de tipos en las interfaces se basa en la forma del objeto, es decir, si el objeto es compatible con los campos y funciones de la interfaz, y no en si la interfaz se implementa o no.
interface IKickable {
kick(distance: number): void;
}
class Ball {
kick(distance: number): void {
console.log("Kicked", distance, "meters!");
}
}
let kickable: IKickable = new Ball();
kickable.kick(40);
Entonces, incluso si Ball
no implementa explícitamente IKickable
, se puede asignar una instancia de Ball
a (y manipularse como) un IKickable
, incluso cuando se especifica el tipo.