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.
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
.
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
afalse
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).
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.
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.
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.
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.
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
onumber
.
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.
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.
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.
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.
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 clasePerson
y una interfazPerson
, entonces el tipo finalPerson
(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
.
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
.