Una interfaz es una forma de un objeto. Un objeto estándar de JavaScript es un mapa de pares key:value. Las claves de los objetos JavaScript en casi todos los casos son cadenas y sus valores son cualquier valor JavaScript soportado (primitivo o abstracto).

Una interfaz indica al compilador de TypeScript los nombres de las propiedades que puede tener un objeto y sus correspondientes tipos de valores. Por lo tanto, la interfaz es un tipo y es un tipo abstracto ya que se compone de tipos primitivos.

Cuando definimos un objeto con propiedades (claves) y valores, TypeScript crea una interfaz implícita mirando los nombres de las propiedades y el tipo de datos de sus valores en el objeto. Esto ocurre debido a la inferencia de tipos.

(object-shape.ts)

En el ejemplo anterior, hemos creado un objeto student con los campos firstName, lastName, age y getSalary y le hemos asignado algunos valores iniciales. Usando esta información, TypeScript crea un tipo de interfaz implícito para student.

{
firstName: string;
lastName: string;
age: number;
getSalary: (base: number) => number;
}

Una interfaz es igual que un objeto pero sólo contiene la información sobre las propiedades del objeto y sus tipos. También podemos crear un tipo de interfaz y darle un nombre para poder usarlo para anotar valores de objetos pero aquí, esta interfaz no tiene un nombre ya que fue creada implícitamente. Puedes comparar esto con el tipo de función en la lección anterior que fue creado implícitamente al principio y luego creamos un tipo de función explícitamente usando el alias de tipo.

Intentemos meternos con las propiedades del objeto después de que fue definido.

(shape-override.ts)

Como puedes ver en el ejemplo anterior, TypeScript recuerda la forma de un objeto ya que el tipo de ross es la interfaz implícita. Si intentamos anular el valor de una propiedad con un valor de tipo diferente al especificado en la interfaz o intentamos añadir una nueva propiedad que no esté especificada en la interfaz, el compilador de TypeScript no compilará el programa.

Si quieres que un objeto tenga básicamente cualquier propiedad, entonces puedes marcar explícitamente un valor any y el compilador de TypeScript no inferirá el tipo a partir del valor del objeto asignado. Hay otras formas mejores de conseguir exactamente esto y las repasaremos en este artículo.

(shape-override-any.ts)

Declaración de interfaz

Aunque la interfaz implícita que hemos visto hasta ahora es técnicamente un tipo, pero no fue definida explícitamente. Como se ha comentado, una interfaz no es más que la forma que puede adoptar un objeto. Si tienes una función que acepta un argumento que debería ser un objeto pero de una forma determinada, entonces tenemos que anotar ese argumento (parámetro) con un tipo de interfaz.

(argumento-con-forma.ts)

En el ejemplo anterior, hemos definido una función getPersonInfo que acepta un argumento objeto que tiene firstName, lastName, age y getSalary campos de tipos de datos especificados. Fíjate que hemos utilizado un objeto que contiene nombres de propiedades y sus correspondientes tipos como tipo utilizando la anotación :<type>. Este es un ejemplo de una interfaz anónima ya que la interfaz no tiene un nombre, se utilizó inline.

Todo esto parece un poco complicado de manejar. Si el objeto ross se complica y necesita ser usado en múltiples lugares, TypeScript parece una cosa que te gustaba inicialmente pero que ahora es algo difícil de manejar. Para resolver este problema, definimos un tipo de interfaz utilizando la palabra clave interface.

(argumento-con-interfaz.ts)

En el ejemplo anterior, hemos definido una interfaz Person que describe la forma de un objeto, pero esta vez, tenemos un nombre que podemos utilizar para referirnos a este tipo. Hemos utilizado este tipo para anotar la variable ross así como el argumento person de la función getPersonIfo. Esto informará a TypeScript para validar estas entidades contra la forma de Person.

¿Por qué utilizar una interfaz?

El tipo de interfaz puede ser importante para hacer cumplir una forma particular. Típicamente en JavaScript, ponemos fe ciega en tiempo de ejecución en que un objeto siempre contendrá una propiedad particular y esa propiedad siempre tendrá un valor de un tipo particular como {age: 21, ...} como ejemplo.

Cuando realmente empezamos a realizar operaciones en esa propiedad sin comprobar primero si esa propiedad existe en el objeto o si su valor es el que esperábamos, las cosas pueden ir mal y puede dejar tu aplicación inutilizable después. Por ejemplo, {age: '21', ...}, aquí age el valor es un string.

Las interfaces proporcionan un mecanismo seguro para hacer frente a estos escenarios en tiempo de compilación. Si accidentalmente estás usando una propiedad en un objeto que no existe o usando el valor de una propiedad en la operación ilegal, el compilador de TypeScript no compilará tu programa. Veamos un ejemplo.

(interface-safety.ts)

En el ejemplo anterior, estamos intentando utilizar la propiedad name del argumento _student dentro de la función printStudent. Como el argumento _student es un tipo de la interfaz Student, el compilador de TypeScript lanza un error durante la compilación ya que esta propiedad no existe en la interfaz Student.

De forma similar, 100 — _student.firstName no es una operación válida ya que la propiedad firstName es un tipo de string y la última vez que lo comprobé, no se puede restar un string de un number es JavaScript (da como resultado NaN).

En el ejemplo anterior, hemos utilizado la forma tradicional de escribir tipo de función para el campo getSalary. Sin embargo, también se puede utilizar la sintaxis de función sin el cuerpo para la misma, que se utiliza generalmente en las interfaces.

interface Student {
firstName: string;
lastName: string;
age: number;
getSalary(base: number): number;
};

Propiedades opcionales

A veces, se necesita que un objeto tenga una propiedad que contenga datos de un tipo de datos particular, pero no es obligatorio tener esa propiedad en el objeto. Esto es similar a los parámetros de función opcionales que aprendimos en la lección anterior.

Estas propiedades se llaman propiedades opcionales. Una interfaz puede contener propiedades opcionales y utilizamos la anotación ?:Type para representarlas, al igual que los parámetros de función opcionales.

(optional-properties.ts)

En el ejemplo anterior, la interfaz Student tiene la propiedad age que es opcional. Sin embargo, si la propiedad age se proporciona, debe tener un valor del tipo number.

En el caso del objeto ross que es un tipo de la interfaz Student, no hemos proporcionado el valor de la propiedad age que es legal, sin embargo, en el caso de monica, hemos proporcionado la propiedad age pero su valor es string que no es legal. Por lo tanto, el compilador de TypeScript lanza un error.

El error puede parecer extraño pero en realidad tiene sentido. Si la propiedad age no existe en un objeto, el object.age devolverá undefined que es un tipo de undefined. Si existe, entonces el valor debe ser del tipo number.

Por tanto, el valor de la propiedad age puede ser del tipo undefined o number que en TypeScript se representa utilizando la sintaxis de unión number | undefined.

💡 Aprenderemos las uniones de tipos en una lección de Type System.

Sin embargo, las propiedades opcionales plantean serios problemas durante la ejecución del programa. Imaginemos que estamos utilizando la propiedad age en una operación aritmética pero su valor es undefined. Este es un tipo de problema serio.

Pero lo bueno es que el compilador de TypeScript no permite realizar operaciones ilegales sobre una propiedad opcional ya que su valor puede ser undefined.

(optional-properties-safety.ts)

En el ejemplo anterior, estamos realizando una operación aritmética sobre la propiedad age que es ilegal porque el valor de esta propiedad puede ser number o undefined en tiempo de ejecución. Realizar operaciones aritméticas en undefined da como resultado NaN (no es un número).

💡 Sin embargo, para el programa anterior, tuvimos que establecer la bandera --strictNullChecks a false que es una bandera del compilador de TypeScript. Si proporcionamos esta opción, el programa anterior compila sin problemas.

Para evitar este error o advertencia, tenemos que decirle explícitamente al compilador de TypeScript que esta propiedad es un tipo de number y no el number o undefined. Para ello, utilizamos la aserción de tipo (AKA conversión de tipo o typecasting).

(optional-properties-safety-override.ts)

En el programa anterior, hemos utilizado (_student.age as number) que convierte el tipo de _student.age de number | undefined a number. Esta es una manera de decirle al compilador de TypeScript, «Hey, esto es un número». Pero una mejor manera de manejar esto sería también comprobar si _student.age es undefined en tiempo de ejecución y luego realizar la operación aritmética.

💡 Aprenderemos sobre las aserciones de tipo en una lección de Sistema de Tipos.

Tipo de función usando una interfaz

No sólo la forma de un objeto plano, sino que una interfaz también puede describir la firma de una función. En la lección anterior, utilizamos alias de tipo para describir un tipo de función, pero las interfaces también pueden hacerlo.

interface InterfaceName {
(param: Type): Type;
}

La sintaxis para declarar una interfaz como un tipo de función es similar a la firma de la función misma. Como puedes ver en el ejemplo anterior, el cuerpo de la interfaz contiene la firma exacta de una función anónima, sin el cuerpo por supuesto. Aquí los nombres de los parámetros no importan.

(function-signature.ts)

En el ejemplo anterior, hemos definido la interfaz IsSumOdd que define un tipo de función que acepta dos argumentos de tipo number y devuelve un valor boolean. Ahora puedes usar este tipo para describir una función porque el tipo de interfaz IsSumOdd es equivalente al tipo de función (x: number, y: number) => boolean.

Una interfaz con una firma de método anónimo describe una función. Pero una función en el ámbito de JavaScript es también un objeto, lo que significa que puedes añadir propiedades a un valor de la función al igual que un objeto. Por lo tanto es perfectamente legal que puedas definir cualquier propiedad en una interfaz del tipo función.

(function-signature-with-methods.ts)

En el ejemplo anterior, hemos añadido las propiedades type y calculate en la interfaz IsSumOdd que describe una función. Usando el método Object.assign, estamos fusionando las propiedades type y calculate con el valor de una función.

Las interfaces del tipo función pueden ser útiles para describir funciones constructoras. Una función constructora es similar a una clase cuyo trabajo es crear objetos (instancias). Sólo teníamos funciones constructoras hasta ES5 para imitar un class en JavaScript. Por lo tanto, TypeScript compila las clases a funciones constructoras si está apuntando a ES5 o menos.

💡 Si quieres aprender más sobre la función constructora, sigue este artículo.

Si ponemos la palabra clave new antes de la firma de la función anónima en la interfaz, hace que la función sea construible. Eso significa que la función sólo puede ser invocada usando la palabra clave new para generar objetos y no usando una llamada de función regular. Un ejemplo de función constructora tiene el siguiente aspecto.

function Animal( _name ) {
this.name = _name;
}var dog = new Animal( 'Tommy' );
console.log( dog.name ); // Tommy

Por suerte, no tenemos que trabajar con funciones constructoras ya que TypeScript proporciona la palabra clave class para crear una clase que es mucho más fácil de trabajar que una función constructora, créeme. De hecho, un class en el fondo es una función constructora en JavaScript. Prueba el siguiente ejemplo.

class Animal{
constructor( _name ) {
this.name = _name;
}
}console.log( typeof Animal ); // "function"

Una clase y una función constructora son la misma cosa. La única diferencia es que el class nos da rica sintaxis OOP para trabajar. Por lo tanto, una interfaz de un tipo de función constructora representa una clase.

(function-signature-with-new.ts)

En el ejemplo anterior, hemos definido la clase Animal con una función constructora que acepta un argumento de tipo string. Usted puede considerar esto como una función constructora que tiene una firma similar del constructor Animal.

El AnimalInterface define una función constructora ya que tiene función anónima prefijada con la palabra clave new. Esto significa que la clase Animal califica para ser un tipo de AnimalInterface. Aquí, el tipo de interfaz AnimalInterface es equivalente al tipo de función new (sound: string) => any.

La función createAnimal acepta ctor argumento de tipo AnimalInterface, por lo tanto podemos pasar la clase Animal como el valor del argumento. No podremos añadir la firma del método getSound de la clase Animal en AnimalInterface y la razón se explica en la lección de Clases.

Tipos indexables

Un objeto indexable es un objeto cuyas propiedades se pueden acceder utilizando una firma de índice como obj. Esta es la forma predeterminada de acceder a un elemento de la matriz, pero también podemos hacer esto para el objeto.

var a = ;
var o = { one: 1, two: 2, three: 3 };console.log( a ); // 1
console.log( a ); // 1 (same as `a.one`)

A veces, su objeto puede tener un número arbitrario de propiedades sin ninguna forma definida. En ese caso, puede utilizar simplemente el tipo object. Sin embargo, este tipo object define cualquier valor que no sea number, string, boolean, symbol, null, o undefined como se discutió en la lección de tipos básicos.

Si necesitamos comprobar estrictamente si un valor es un objeto de JavaScript plano entonces podríamos tener un problema. Esto se puede resolver utilizando un tipo de interfaz con una firma de índice para el nombre de la propiedad.

interface SimpleObject {
: any;
}

La interfaz SimpleObject define la forma de un objeto con string claves cuyos valores pueden ser de tipo de datos any. Aquí, el nombre de la propiedad key sólo se utiliza para el marcador de posición, ya que está encerrado entre corchetes.

(indexable-type-object.ts)

En el ejemplo anterior, hemos definido ross y monica objeto de tipo SimpleObject interfaz. Dado que estos objetos contienen claves string y valores de tipo de datos any, es perfectamente legal.

Si te confundes con la clave 1 en el monica que es de tipo number, esto es legal ya que los elementos de los objetos o arrays en JavaScript pueden ser indexados usando claves number o string, como se muestra a continuación.

var o = { 0: 'Zero', '1': 'One' };
var a = ;console.log( o ); // Zero
console.log( o ); // One
console.log( a ); // One
console.log( a ); // One

Si necesitamos ser más precisos sobre el tipo de claves y sus valores, seguramente también podemos hacerlo. Por ejemplo, podemos definir un tipo de interfaz indexable con claves de tipo number y valores de tipo number si queremos.

💡 Un tipo de clave de firma de índice debe ser string o number.

(indexable-type-number.ts)

En el ejemplo anterior, hemos definido una interfaz LapTimes que puede contener nombres de propiedades de tipo number y valores de tipo number. Esta interfaz puede representar una estructura de datos que puede ser indexada usando claves number por lo tanto el array ross y los objetos monica y joey son legales.

Sin embargo, el objeto rachel no cumple con la forma de LapTimes ya que la clave one es un string y sólo se puede acceder usando string como rachel y nada más. Por lo tanto el compilador de TypeScript lanzará un error como se muestra arriba.

Es posible tener algunas propiedades requeridas y otras opcionales en un tipo de interfaz indexable. Esto puede ser bastante útil cuando necesitamos que un objeto tenga una forma determinada pero realmente no importa si obtenemos propiedades extra y no deseadas en el objeto.

(indexable-type-required-properties.ts)

En el ejemplo anterior, hemos definido una interfaz LapTimes que debe contener la propiedad name con valor string y la propiedad opcional age con valor number. Un objeto de tipo LapTimes también puede tener propiedades arbitrarias cuyas claves deben ser number y cuyos valores también deben ser number.

El objeto ross es un objeto LapTimes válido aunque no tenga la propiedad age ya que es opcional. Sin embargo, monica sí tiene la propiedad age pero su valor es string por lo que no cumple con la interfaz LapTimes.

El objeto joey tampoco cumple con la interfaz LapTimes ya que tiene una propiedad gender que es un tipo de string. El objeto rachel no tiene la propiedad name que se requiere en la interfaz LapTimes.

💡 Hay algunas trampas que debemos tener en cuenta al utilizar tipos indexables. Estos se mencionan en esta documentación.

Extender la interfaz

Al igual que las clases, una interfaz puede heredar propiedades de otras interfaces. Sin embargo, a diferencia de las clases en JavaScript, una interfaz puede heredar de múltiples interfaces. Utilizamos la palabra clave extends para heredar una interfaz.

(extend-interface.ts)

Al extender una interfaz, la interfaz hija obtiene todas las propiedades de la interfaz padre. Esto es bastante útil cuando varias interfaces tienen una estructura común y queremos evitar la duplicación de código sacando las propiedades comunes en una interfaz común que pueda ser heredada posteriormente.

(extend-interfaces-múltiples.ts)

En el ejemplo anterior, hemos creado una interfaz Student que hereda propiedades de la interfaz Person y Player.

Declaraciones de interfaz múltiple

En la sección anterior, aprendimos cómo una interfaz puede heredar las propiedades de otra interfaz. Esto se hizo utilizando la palabra clave extend. Sin embargo, cuando se declaran interfaces con el mismo nombre dentro del mismo módulo (archivo), TypeScript fusiona sus propiedades siempre que tengan nombres de propiedades distintos o que sus tipos de propiedades en conflicto sean los mismos.

(merged-interface.ts)

En el ejemplo anterior, hemos declarado la interfaz Person varias veces. Esto dará como resultado una única declaración de interfaz Person al fusionar las propiedades de todas las declaraciones de interfaz Person.

💡 En la lección de Clases, hemos aprendido que una class declara implícitamente una interfaz y una interfaz puede extender esa interfaz. Así que si un programa tiene una clase Person y una interfaz Person, entonces el tipo final Person (interfaz) tendrá propiedades fusionadas entre la clase y la interfaz.

Interfaces anidadas

Una interfaz puede tener estructuras profundamente anidadas. En el siguiente ejemplo, el campo info de la interfaz Student define la forma de un objeto con propiedades firstName y lastName.

(nested-shape.ts)

Así mismo, es perfectamente legal que un campo de una interfaz tenga el tipo de otra interfaz. En el siguiente ejemplo, el campo info de la interfaz Student tiene el tipo de la interfaz Person.

(nested-interface.ts)

Deja una respuesta

Tu dirección de correo electrónico no será publicada.