La compatibilidad de tipos en TypeScript se basa en subtipos estructurales. La verificación estructural de tipos es una forma de vincular tipos según sus elementos únicamente. Esto es lo opuesto a la tipificación nominal, veamos el siguiente código:
Tabla de contenidos
- Características
- Compara dos funciones
- Múltiples constantes (enum)
- Categorías
- Tipos generalizados
- Temas avanzados
- Recursos del Artículo
interface Named { name: string; } class Person { name: string; } let p: Named; // Esto está permitido porque los tipos se verifican estructuralmente p = new Person();
En los lenguajes que se basan en la verificación de tipos nominales, como C# o Java, el código correspondiente será falso porque la clase Person
no se describe a sí misma explícitamente como un implementador de interfaz Named
.
El sistema de verificación de tipos está estructurado en TypeScript según la forma en que se escribe JavaScript. Dado que JavaScript utiliza con frecuencia objetos anónimos, como expresiones de función y objetos literales, es normal representar dichas relaciones en bibliotecas de JavaScript con un sistema de verificación de tipos de forma jerárquica en lugar de un sistema nominal.
Nota sobre la solidez
El sistema de tipos de TypeScript permite que algunas operaciones que no son en tiempo de compilación sean seguras. Cuando un sistema de tipos tiene esta propiedad, decimos que no es sólido. Los casos en los que TypeScript permite este tipo de operaciones no lineales se han seleccionado cuidadosamente y en esta página explicaremos dónde y por qué ocurren estas operaciones.
Características
La regla general para la jerarquía de TypeScript es que un simétrico x es compatible si y
tiene los mismos elementos que x
, por ejemplo:
interface Named { name: string; } let x: Named; // Escribe año // inferido // { name: string; location: string; } let y = { name: "Alice", location: "Seattle" }; x = y;
Para verificar si una variable y es asignable a x, el compilador verificará cada una de sus propiedades en x
para encontrar una propiedad equivalente en y
. En este caso, la variable y
debe tener un elemento cuyo type name
sea una cadena(string). Por serlo se permitirá la cita. La misma regla de asignación se utiliza al verificar los valores dados a los parámetros de una función cuando se llama:
function greet(n: Named) { console.log("Hello, " + n.name); } greet(y); // OK
Tenga en cuenta que el objeto y tiene una propiedad denominada location
, pero esto no genera un error. Esto se debe a que solo los elementos del tipo de destino se tienen en cuenta cuando se comprueba la compatibilidad (los elementos del tipo Named
en este caso).
Nota: El tipo de destino es el que se va a asignar (a la izquierda de la asignación) y el tipo de origen es el tipo asignado (a la derecha de la asignación). La comparación procede recursivamente explorando el tipo de cada elemento y cada niño.
Compara dos funciones
Comparar tipos primitivos y tipos de objetos es relativamente sencillo, pero la compatibilidad entre funciones es un poco más compleja.Comencemos con un ejemplo inicial de dos funciones que difieren solo en su lista de parámetros:
let x = (a: number) => 0; let y = (b: number, s: string) => 0; y = x; // permitido x = y; // Error
Para verificar si una función x
es asignable a una variable y
, primero miramos la lista de parámetros. Cada parámetro en un debe tener un parámetro equivalente de un tipo compatible en el otro. Tenga en cuenta que los nombres de los parámetros no se tienen en cuenta, sólo importan sus tipos. En este caso, cada parámetro en x tiene un parámetro equivalente correspondiente en la lista de parámetros en y
, por lo que se permite la asignación. La segunda asignación es falsa, porque la función y
tiene un segundo parámetro solicitado que no está en la lista de parámetros x
, por lo que la asignación no está permitida.
Quizás se pregunte por qué se permite deshacerse de parámetros como en el ejemplo y = x
. La razón es que ignorar los parámetros de función finales es muy común en JavaScript. Por ejemplo, la función Array#forEach
proporciona tres parámetros a la función de devolución de llamada: el elemento de la matriz, su índice y la matriz que contiene el elemento. Sin embargo, es muy útil proporcionar una devolución de llamada(callback function) que use solo el primer parámetro:
let items = [1, 2, 3]; // Estos operadores generales no tienen que ser items.forEach((item, index, array) => console.log(item)); // Tome el primer operando e ignore los operandos sin importancia items.forEach(item => console.log(item));
Ahora veamos cómo manejar los tipos de devolución. Tome el siguiente ejemplo, que contiene dos funciones que difieren solo en sus tipos de devolución:
let x = () => ({name: "Alice"}); let y = () => ({name: "Alice", location: "Seattle"}); x = y; // Permitido // Falso porque en x() // falta una propiedad llamada location y = x;
El sistema de tipos dicta que el tipo de valor devuelto de la función de origen debe ser un subtipo del tipo de valor devuelto de destino.
Función Parámetro Varianza
Cuando se comparan tipos de parámetros de función, la asignación tiene éxito si el parámetro de origen se puede asignar al parámetro de destino o viceversa. Esto no es sencillo porque a la persona que llama a la función se le puede dar una función que toma un tipo más especializado y llama a la función de un tipo menos especializado. Este tipo de error es prácticamente raro y permitir este proceso ayuda con el uso de muchos patrones comunes de JavaScript, veamos un ejemplo:
enum EventType { Mouse, Keyboard } interface Event { timestamp: number; } interface MouseEvent extends Event { x: number; y: number } interface KeyEvent extends Event { keyCode: number } function listenEvent(eventType: EventType, handler: (n: Event) => void) { /* ... */ } // No es sencillo, pero es útil y común listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + "," + e.y)); // Alternativas directas pero no deseadas listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + "," + (<MouseEvent>e).y)); listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + "," + e.y))); // No permitido, las reglas de seguridad de tipos se aplican aquí porque los dos tipos no listenEvent(EventType.Mouse, (e: number) => console.log(e));
Parámetros opcionales y parámetros de descanso
Al comparar la compatibilidad de funciones, los operandos opcionales y requeridos son conmutativos (es decir, no hay diferencia entre ellos). Los operandos opcionales en exceso en el tipo de origen no son falsos, y los operandos opcionales del tipo de destino sin operandos equivalentes en el tipo de origen tampoco son falsos. Cuando una función tiene operandos residuales, se trata como si fuera una serie infinita de operandos opcionales.
Esto no es sencillo desde la perspectiva del tipo de sistema, pero desde la perspectiva del tiempo de ejecución, la noción de operandos opcionales no se implementa muy a menudo porque es el equivalente a pasar el valor undefined
en mayoría de los casos.
La razón para decidir esta operación no lineal es el patrón común de una función que recibe una devolución de llamada y la llama con una cantidad de operandos que el programador espera pero que no conoce en el nivel del tipo de sistema:
function invokeLater(args: any[], callback: (...args: any[]) => void) { // Aquí la devolución de llamada con parámetros se llama 'args' } // no es directo porque invocarLater // puede proporcionar cualquier cantidad de parámetros invokeLater([1, 2], (x, y) => console.log(x + ", " + y)); // Esto es ambiguo porque los parámetros (x, y) // en realidad son necesarios pero no pueden ser pasados invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));
Funciones con sobrecargas
Cuando una función tiene sobrecargas, cada sobrecarga del tipo de origen debe coincidir con una firma correspondiente en el tipo de destino. Esto garantiza que la función de destino se pueda llamar en los mismos estados que la función de origen.
Múltiples constantes (enum)
Las constantes múltiples son compatibles con los números, y los números son compatibles con las constantes múltiples. Sin embargo, los valores de múltiples constantes que son de diferentes tipos de múltiples constantes no son compatibles, veamos un ejemplo a continuación:
enum Status { Ready, Waiting }; enum Color { Red, Blue, Green }; let status = Status.Ready; status = Color.Green; // Error
Categorías
Las clases funcionan de manera similar a los tipos de objetos literales y similares a las interfaces con una excepción: las clases tienen un tipo estático y un tipo de instancia. Al comparar dos objetos de un determinado tipo de clase, solo se comparan los miembros de copia. Los elementos y constructores estáticos no afectan a la asignación.
class Animal { feet: number; constructor(name: string, numFeet: number) { } } class Size { feet: number; constructor(numFeet: number) { } } let a: Animal; let s: Size; a = s; // permitido s = a; // permitido
Miembros privados y miembros protegidos en clases
Los elementos privados y protegidos en una clase afectan la compatibilidad. Al comprobar la compatibilidad de la instancia de una clase, si el tipo de destino contiene un elemento privado, el tipo de origen también debe contener un elemento especial cuya raíz sea la misma clase. Esto también se aplica a una copia con un objeto protegido. Esto permite que la asignación de un taxón sea compatible con la variedad original de la que se deriva, pero no con otras clases que tengan una línea hereditaria diferente de la misma forma.
Tipos generalizados
Dado que TypeScript es un sistema de tipo jerárquico, los parámetros de tipo solo afectan al tipo resultante cuando se procesa como parte del tipo de un elemento determinado. por ejemplo:
interface Empty<T> { } let x: Empty<number>; let y: Empty<string>; x = y; // Permitidos porque tienen la misma estructura
En el ejemplo anterior, (x
, y
) son compatibles porque sus estructuras no usan el tipo dado de manera diferente. Cambiando este ejemplo agregando un elemento para Empty<T>
ilustrar la idea:
interface NotEmpty<T> { data: T; } let x: NotEmpty<number>; let y: NotEmpty<string>; // Falso porque x e y // incompatibles x = y;
De esta forma, un tipo generalizado con datos de ciertos tipos se comporta igual de bien que un tipo no generalizado. Para los tipos generalizados para los que no se especifican tipo de argumentos, la compatibilidad se comprueba especificando any
como parámetro, para todos los parámetros de tipos no definidos. A continuación, se comprueba la compatibilidad de los tipos resultantes, al igual que con los tipos no generalizados, por ejemplo:
let identity = function<T>(x: T): T { // ... } let reverse = function<U>(y: U): U { // ... } // permitido porque (x: cualquiera) => cualquiera // coincide (y: cualquiera) => cualquiera identity = reverse;
Temas avanzados
Subtipo de asignación
En esta página usamos el término «compatible», que no está definido en la especificación del lenguaje. En TypeScript hay dos formas de compatibilidad: subtipo y asignación, estos solo se diferencian en que la asignación amplía la compatibilidad de subtipo con reglas adicionales que permiten asignar desde type any
hasta enum
con valores escalares equivalentes.
Uno de los dos métodos combinatorios se usa en diferentes partes del idioma dependiendo de la situación. Incluso en los casos de las cadenas implements
y extends
. Para obtener más información, consulte la especificación del lenguaje TypeScript.
Recursos del Artículo
- Categorías en TypeScript
- Declaración de Variables en TypeScript
- Funciones en TypeScript
- Inferir Tipos en TypeScript
- Interfaces en TypeScript
- Introducción a TypeScript
- Iteradores y generadores en TypeScript
- Múltiples Constantes en TypeScript
- Símbolo en TypeScript (Symbol)
- Tipos Avanzados en TypeScript
- Tipos Básicos de Datos en TypeScript
- Tipos de Compatibilidad en TypeScript
- Tipos Generalizados (Generics) en TypeScript