El enfoque de verificación de tipos en los valores de forma es uno de los principios básicos de TypeScript. A veces se denomina tipificación pato o subtipificación estructural. Las interfaces en TypeScript nombran estos tipos, que es una forma poderosa de definir contratos dentro de su código o contratos con código fuera de su proyecto.
Tabla de contenidos
- Interfaz sencilla
- Características opcionales
- Propiedades de solo lectura
- Exceso de propiedad (Excess Property Checks)
- Tipos de funciones
- Tipos indexables
- Tipos de clase
- Expansión de interfaces
- Tipos híbridos
- Ampliar categorías con interfaces
- Recursos del Artículo
Interfaz sencilla
Comencemos con un ejemplo simple para entender cómo funcionan las interfaces:
function printLabel(labelledObj: { label: string }) { console.log(labelledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
El verificador de tipos verificará la llamada al printLabel
. La función printLabel tiene un parámetro que requiere que el objeto que se pasa contenga una propiedad cuyo label
sea tipo string
. Tenga en cuenta que el objeto que le pasamos a la función cuando se llama tiene más propiedades que las requeridas, pero el compilador verifica que existan al menos las propiedades requeridas y que coincidan con el tipo requerido.
Hay algunos casos en los que TypeScript no permite que un objeto sea ligeramente diferente de lo que se requiere (como incrementar una propiedad), y lo cubriremos en un momento. Podríamos volver a escribir el mismo ejemplo, pero esta vez usaremos una interfaz para describir el requisito de tener una propiedad cuyo tipo label
sea una cadena:
interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
Ahora podemos usar el nombre de la interfaz LabelledValue
para describir requisito en el ejemplo anterior. Este requisito aún representa tener una propiedad cuyo tipo label
es una cadena (string). Tenga en cuenta que no necesitamos especificar que el objeto que se va a pasar printLabel
implemente esta interfaz como podríamos necesitar en otros lenguajes. Lo importante aquí es solo la forma del objeto. Si el objeto pasado a la función cumple con los requisitos especificados, se permite.
Tenga en cuenta también que la verificación de tipos no requiere que estas propiedades estén en ningún orden en particular, solo requiere que las propiedades solicitadas en la interfaz estén presentes en el objeto y sean del tipo requerido.
Características opcionales
Es posible que algunas funciones de la interfaz no sean necesarias. De modo que algunos de ellos están presentes en ciertos casos y pueden no existir en absoluto. Estas propiedades opcionales son comunes cuando se crean patrones como «bolsas de opciones» en las que se pasa un objeto a una función que ya tiene los valores de alguna propiedad. Veamos un ejemplo de este patrón:
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): {color: string; area: number} { let newSquare = {color: "white", area: 100}; if (config.color) { newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } let mySquare = createSquare({color: "black"});
Las interfaces con propiedades opcionales se escriben como otras interfaces, con el carácter agregado ?
al final del nombre de la propiedad opcional en la declaración.
Lo que hace que las propiedades opcionales sean especiales es que puede describir aquellas propiedades que pueden estar presentes al mismo tiempo que prohíbe el uso de propiedades que no están definidas en la interfaz. Por ejemplo, si escribimos mal la propiedad color
en createSquare
, recibiremos un mensaje de error avisandonos de que el nombre de la propiedad era incorrecto:
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare = {color: "white", area: 100}; if (config.clor) { // Error: Property 'clor' does not exist on type 'SquareConfig' // Error: La propiedad 'clor' no existe en el tipo 'SquareConfig' newSquare.color = config.clor; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } let mySquare = createSquare({color: "black"});
Propiedades de solo lectura
Algunas propiedades solo deben poder cambiarse cuando el objeto se crea por primera vez. Puede especificar propiedades como inmutables y de solo lectura colocando la palabra clave readonly
antes del nombre de la propiedad:
interface Point { readonly x: number; readonly y: number; }
Puede construir un objeto de tipo Point
asignando un valor literal de objeto. El valor de (x, y) no se puede cambiar después de la asignación.
let p1: Point = { x: 10, y: 20 }; p1.x = 5; // Error, no puede asignar un nuevo valor a una propiedad de solo lectura
TypeScript proporciona el tipo ReadonlyArray<T>
similar a Array<T>
, omitiendo todos los métodos de mutación, por lo que puede estar seguro de que las matrices no cambian después de haberlas creado:
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = a; ro[0] = 12; // error ro.push(5); // error ro.length = 100; // error a = ro; // error
Puede notar en la última línea de arriba que no está permitido reasignar toda la matriz de tipo ReadonlyArray
a una matriz regular. Pero aún puede reemplazarlo con una afirmación de tipo:
a = ro as number[];
Readonly vs const
La forma más fácil de recordar cuándo usar readonly
y const
es preguntar si está trabajando con una variable o una propiedad. Se utilizan variables será const
y se utilizan propiedades entonces será readonly
.
Exceso de propiedad (Excess Property Checks)
En el primer ejemplo de interfaz anterior, TypeScript nos permitió pasar un { size: number; label: string; }
a algo que solo esperaba una propiedad con el { label: string; }
. Acabamos de aprender acerca de las funciones opcionales y cómo son útiles al describir las llamadas bolsas de opciones.
Pero integrar ambos principios sin pensar resultará en un dolor de cabeza como lo sería si estuvieras usando JavaScript. Por ejemplo, tomemos createSquare
el ejemplo anterior :
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { // ... } let mySquare = createSquare({ colour: "red", width: 100 });
Observe que el parámetro pasado a la función createSquare
se escribe colour
en vez de color
. Un error como este en JavaScript pasará silenciosamente sin previo aviso. Podría decir que este programa es correcto porque las dos propiedades width son compatibles, no hay ninguna propiedad con nombre color y el atributo final colour no funcionará.
Pero TypeScript cree que hay un error en este código. Los valores de los objetos literales pasan por una verificación de propiedad de desbordamiento cuando se asignan a otras variables o cuando se pasan como parámetros. Si el valor de un objeto literal tiene una propiedad que el tipo de destino no tiene, obtendrá un error:
// Falso, propiedad 'color' // no se espera en tipo 'SquareConfig' let mySquare = createSquare({ colour: "red", width: 100 });
Omitir esta verificación de funciones adicionales es muy simple. La forma más fácil de hacer esto es usando una aserción de tipo de la siguiente manera:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
Pero podría haber una mejor manera si está seguro de que el objeto puede tener propiedades redundantes que se usarán de forma privada, agregando una firma de índice de cadena si la interfaz SquareConfig
puede tener ambas propiedades color
y width
de los tipos anteriores, pero también puede tener cualquier número de otras propiedades, entonces podemos definir la interfaz de la siguiente manera:
interface SquareConfig { color?: string; width?: number; [propName: string]: any; }
Hablaremos sobre las firmas de índice en un momento. Lo importante es que estamos especificando que ‘ SquareConfig
puede tener cualquier número de propiedades, mientras no sea un color
y su tipo width no importa.
Finalmente, el objeto se puede establecer en otra variable para anular esta verificación, un método que podría sorprender un poco, porque squareOptions
no pasará por una verificación de propiedad sobrecargada y el compilador no arrojará un error:
let squareOptions = { colour: "red", width: 100 }; let mySquare = createSquare(squareOptions);
Recuerde que probablemente no tenga que omitir estas comprobaciones si el código es tan simple como en el ejemplo anterior. Sin embargo, es posible que necesite estas técnicas cuando trabaje con objetos complejos que tienen hijos y estado, pero la mayoría de los errores en las propiedades adicionales son en realidad errores. Esto significa que si obtiene tales errores al confiar en paquetes de opciones o similares, es posible que deba revisar sus declaraciones de tipo. En este caso, si al pasar un objeto con una propiedad llamada color
o colour
no causa a createSquare
ningún problema, debe corregir la definición de SquareConfig
.
Tipos de funciones
Las interfaces pueden describir una amplia gama del aspecto que podrían tener los objetos de JavaScript. Las interfaces describen tipos de funciones, así como describen un objeto y sus propiedades.
Para describir el tipo de una función usando una interfaz, le damos a la interfaz una firma de llamada. Es similar a declarar una función con solo la lista de parámetros y el tipo de retorno. Cada parámetro en la lista de parámetros requiere un nombre y tipo:
interface SearchFunc { (source: string, subString: string): boolean; }
Después de definir esta interfaz de tipo de función, podemos usarla como usamos cualquier otra interfaz. El siguiente ejemplo muestra cómo crear una variable del tipo de una función y asignarla a un valor de función del mismo tipo:
let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { let result = source.search(subString); return result > -1; }
No es necesario que los nombres de los parámetros sean idénticos para que los tipos de función se validen correctamente. Por ejemplo, podríamos escribir el ejemplo anterior como:
let mySearch: SearchFunc; mySearch = function(src: string, sub: string): boolean { let result = src.search(sub); return result > -1; }
Los operandos de las funciones se verifican uno por uno y el tipo se verifica en la ubicación del operando con su tipo de parámetro correspondiente. Si no desea especificar tipos, la escritura contextual en TypeScript puede inferir tipos de parámetros porque el valor de la función se asigna directamente a una variable de tipo SearchFunc
. Y el tipo de valor devuelto en la expresión de función aquí está determinado por el valor que devuelve (el valor false
o el valor true
en este caso). Si la expresión de la función devuelve un número o una cadena, el verificador de tipos nos alerta de que el valor devuelto no coincide con el valor devuelto descrito en la interfaz SearchFunc
.
let mySearch: SearchFunc; mySearch = function(src, sub) { let result = src.search(sub); return result > -1; }
Tipos indexables
Podemos describir de manera similar los tipos con los que podemos usar la indexación como a[10]
o ageMap["daniel"]
. Los tipos indexables tienen una firma de índice que describe los tipos que podemos usar para indexar un objeto, así como los tipos devueltos al indexar. Veamos el siguiente ejemplo:
interface StringArray { [index: number]: string; } let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0];
En el ejemplo anterior, tenemos una interfaz llamada conmutador StringArray
con una firma de índice. Esta firma de índice indica que si usa la indexación con una matriz de tipo StringArray
usando un número ( number
), devolverá una cadena ( string
) .
Hay dos tipos de firmas de índice: cadenas y números. Se pueden admitir ambos tipos de indexadores, pero el tipo devuelto por un indexador escalar debe ser un subtipo del tipo devuelto por el indexador de cadenas. Esto se debe a que, al indexar con un número ( number
) , JavaScript lo convertirá en una cadena ( string
) antes de indexar el objeto. Esto significa que indexar por número 100
es mismo que indexar con una cadena "100"
, por lo que ambos deben ser consistentes:
class Animal { name: string; } class Dog extends Animal { breed: string; } // Falso, la indexación con una cadena entera podría obtener un tipo diferente de clase Animal interface NotOkay { [x: number]: Animal; [x: string]: Dog; }
Aunque las firmas de índice de cadena son una forma útil de describir el estilo de los diccionarios, requieren que todas las propiedades coincidan con su tipo de retorno. Esto se debe a que el índice de cadena indica que también está obj.property
disponible como obj["property"]
.
En el siguiente ejemplo, el tipo name
no coincide con el tipo de índice del texto, por lo que el verificador de tipos genera un error:
interface NumberDictionary { [index: string]: number; length: number; // la longitud es un número que está permitido // error, the type of 'name' is not a subtype of the indexer // error, el tipo 'name' no es un subtipo del indexador name: string; }
Finalmente, puede hacer que las firmas de índice sean de solo lectura para evitar que se asignen valores a sus índices:
interface ReadonlyStringArray { readonly [index: number]: string; } let myArray: ReadonlyStringArray = ["Alice", "Bob"]; myArray[2] = "Mallory"; // Error
No puede establecer un valor para el elemento myArray[2]
porque la firma del índice es de solo lectura.
Tipos de clase
Implementando una interfaz
El requisito de que una clase se ajuste a un contrato particular es un uso común de las interfaces en lenguajes como C# y Java, y esto también es posible en TypeScript.
interface ClockInterface { currentTime: Date; } class Clock implements ClockInterface { currentTime: Date; constructor(h: number, m: number) { } }
Las funciones también se pueden describir en la interfaz que implementa la clase, como lo muestra el método setTime
en el siguiente ejemplo:
interface ClockInterface { currentTime: Date; setTime(d: Date); } class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } }
Las interfaces describen el lado público de la clase en lugar del lado privado. Esto evita el uso de interfaces para verificar que una clase también tenga un tipo específico en el lado privado de la instancia de la clase.
Diferencia entre el lado estático y el lado de instancia de las clases
Cuando se trabaja con clases e interfaces, es útil recordar que una clase tiene dos tipos diferentes: el tipo de lado estático(static) y el tipo de lado de instancia(instance) . Puede notar que si crea una interfaz con un constructor e intenta crear una clase que implemente esa interfaz, obtendrá un error:
interface ClockConstructor { new (hour: number, minute: number); } class Clock implements ClockConstructor { currentTime: Date; constructor(h: number, m: number) { } }
Esto se debe a que cuando se implementa una clase de interfaz específica, sólo la instancia la verifica. Debido a que el constructor está en el lado estático(static), no estará cubierto por esta verificación.
En su lugar, deberá trabajar directamente con el lado estático de la clase. En este ejemplo, estamos definiendo dos interfaces, una para el constructor ClockConstructor
y otra para los métodos de instancia del ClockInterface
. Luego definimos una función constructora para que sea createClock
más fácil crear copias del tipo que se le pasa.
interface ClockConstructor { new (hour: number, minute: number): ClockInterface; } interface ClockInterface { tick(); } function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface { return new ctor(hour, minute); } class DigitalClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("beep beep"); } } class AnalogClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("tick tock"); } } let digital = createClock(DigitalClock, 12, 17); let analog = createClock(AnalogClock, 7, 32);
Debido a que el primer createClock es de tipo ClockConstructor
, comprobará que AnalogClock
tiene una firma de sintaxis válida al llamar createClock(AnalogClock, 7, 32)
.
Expansión de interfaces
Las interfaces pueden extenderse entre sí como clases. Esto permite que un elemento de la interfaz se copie en otro, brindando más flexibilidad en la forma en que divide sus interfaces en componentes reutilizables.
interface Shape { color: string; } interface Square extends Shape { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10;
Una interfaz puede extender múltiples interfaces, creando una mezcla de todas las interfaces que extiende:
interface Shape { color: string; } interface PenStroke { penWidth: number; } interface Square extends Shape, PenStroke { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10; square.penWidth = 5.0;
Tipos híbridos
Las interfaces, como ya se mencionó, pueden describir los tipos ricos que existen en JavaScript en la vida real. Debido a la naturaleza dinámica y flexible de JavaScript, es posible que ocasionalmente encuentre un objeto que actúe como una combinación de algunos de los tipos descritos anteriormente. Los objetos que actúan como una función y un objeto al mismo tiempo son ejemplos de esto, junto con propiedades adicionales.
interface Counter { (start: number): string; interval: number; reset(): void; } function getCounter(): Counter { let counter = <Counter>function (start: number) { }; counter.interval = 123; counter.reset = function () { }; return counter; } let c = getCounter(); c(10); c.reset(); c.interval = 5.0;
Es posible que deba usar patrones como los anteriores cuando use JavaScript de terceros para describir cómo se ve todo el tipo.
Ampliar categorías con interfaces
Cuando un tipo de interfaz extiende un tipo de clase, heredará (heredará) los elementos de la clase pero no las interfaces implementadas por la clase. Es como si la interfaz definiera todos los elementos de la clase sin proporcionar ninguna implementación. Las interfaces heredan incluso elementos de clase base que son privados o están protegidos. Esto significa que al crear una interfaz que amplía una clase con elementos privados o protegidos, el tipo de interfaz solo se aplica a esa clase o su subclase.
Esto es útil si desea crear una gran jerarquía de herencia y quiere que su código funcione solo con subclases con ciertas propiedades. Las subclases no necesitan estar relacionadas entre sí, solo es importante que hereden de la clase base. por ejemplo:
class Control { private state: any; } interface SelectableControl extends Control { select(): void; } class Button extends Control implements SelectableControl { select() { } } class TextBox extends Control { select() { } } // Error, propiedad 'state' // falta en el tipo 'Image' class Image implements SelectableControl { select() { } } class Location { }
En el ejemplo anterior, SelectableControl
contiene todos los elementos de Control
, incluida la propiedad privada state
. Debido a que state es un elemento, solo los hijos de la clase (descendientes) Control
pueden implementar la interfaz SelectableControl
. Esto se debe a que solo los elementos secundarios en Control de un elemento privado tendrán un state
elemento privado de la misma declaración, que es un requisito para que los elementos privados sean compatibles.
Se puede acceder al elemento privado state
dentro Control
a través de una copia de SelectableControl
. En la práctica, se comporta SelectableControl
como clase Control
conocida por tener una función llamada select
. Class Button
y class TextBox
son dos subtipos de SelectableControl
( porque ambos heredan de Control y tienen un hijo llamado select
), pero ambas clases Image
y Location
no son subtipos de ellos.
Recursos del Artículo
- Múltiples Constantes en TypeScript
- Iteradores y generadores en TypeScript
- Símbolo en TypeScript (Symbol)
- Tipos Avanzados en TypeScript
- Tipos de Compatibilidad en TypeScript
- Inferir Tipos en TypeScript
- Tipos Generalizados (Generics) en TypeScript
- Tipos Básicos de Datos en TypeScript
- Interfaces en TypeScript
- Declaración de Variables en TypeScript
- Funciones en TypeScript
- Categorías en TypeScript
- Introducción a TypeScript