Escritura nominal en TypeScript o cómo proteger su interfaz de identificadores alienígenas


Recientemente, al estudiar las razones del funcionamiento incorrecto de mi proyecto de casa, una vez más noté un error que a menudo se repite debido a la fatiga. La esencia del error es que, al tener varios identificadores en un bloque de código, cuando llamo a una función, paso el identificador de un objeto de otro tipo. En este artículo, hablaré sobre cómo resolver este problema usando TypeScript.


Poco de teoría


TypeScript se basa en la tipificación estructural, que se adapta bien a la ideología del pato de JavaScript. Se ha escrito un número suficiente de artículos sobre esto. No los repetiré, solo describiré la diferencia principal del tipeo nominativo, que es más común en otros idiomas. Examinemos un pequeño ejemplo.


class Car { id: number; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: number; move (x: number, y: number) { //   } } let car: Car = new Boat(); //  TypeScript   let boat: Boat = new Car(); //        

¿Por qué TypeScript se comporta de esta manera? Esto es solo una manifestación de tipificación estructural. A diferencia de la nominativa, que supervisa los nombres de tipo, la tipificación estructural toma una decisión sobre la compatibilidad de los tipos en función de su contenido. La clase Car contiene todas las propiedades y métodos de la clase Boat, por lo que Car se puede usar como Boat. Lo contrario no es cierto porque Boat no tiene la propiedad numberOfWheels.


Identificadores de escritura


En primer lugar, estableceremos tipos para identificadores


 type CarId: number; type BoatId: number; 

y reescribe las clases usando estos tipos.


 class Car { id: CarId; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: BoatId; move (x: number, y: number) { //   } } 

Notarás que la situación no ha cambiado mucho, porque todavía no tenemos control sobre de dónde obtuvimos el identificador, y tendrás razón. Pero este ejemplo ya ofrece algunas ventajas.


  1. Durante el desarrollo del programa, el tipo de identificador puede cambiar repentinamente. Entonces, por ejemplo, un cierto número de automóvil, exclusivo del proyecto, se puede reemplazar por un número VIN de cadena. Sin especificar el tipo de identificador, tendrá que reemplazar el número con una cadena en todos los lugares donde ocurra. Con la tarea de tipo, el cambio deberá hacerse solo en un lugar donde se determine el tipo en sí.


  2. Cuando llamamos a funciones, obtenemos sugerencias de nuestro editor de código, qué identificadores de tipo deberían ser. Supongamos que tenemos las siguientes funciones declaradas:


     function getCarById(id: CarId): Car { // ... } function getBoatById(id: BoatId): Boat { // ... } 

    Luego, recibiremos una pista del editor de que debemos transmitir no solo un número, sino CarId o BoatId.



Emula la escritura más estricta


No hay escritura nominal en TypeScript, pero podemos emular su comportamiento, haciendo que cualquier tipo sea único. Para hacer esto, agregue una propiedad única al tipo. Este truco se menciona en los artículos en inglés bajo el término Branding, y así es como se ve:


 type BoatId = number & { _type: 'BoatId'}; type CarId = number & { _type: 'CarId'}; 

Después de señalar que nuestros tipos deben ser tanto un número como un objeto con una propiedad con un valor único, hicimos que nuestros tipos fueran incompatibles para comprender la tipificación estructural. Veamos como funciona.


 let carId: CarId; let boatId: BoatId; let car: Car; let boat: Boat; car = getCarById(carId); // OK car = getCarById(boatId); // ERROR boat = getBoatById(boatId); // OK boat = getBoatById(carId); // ERROR carId = 1; // ERROR boatId = 2; // ERROR car = getCarById(3); // ERROR boat = getBoatById(4); // ERROR 

Todo se ve bien, excepto las últimas cuatro líneas. Para crear identificadores, necesita una función auxiliar:


 function makeCarIdFromVin(id: number): CarId { return vin as any; } 

La desventaja de este método es que esta función permanecerá en tiempo de ejecución.


Hacer que la escritura fuerte sea un poco menos estricta


En el último ejemplo, tuve que usar una función adicional para crear el identificador. Puede deshacerse de él utilizando la definición de interfaz Flavor:


 interface Flavoring<FlavorT> { _type?: FlavorT; } export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>; 

Ahora puede establecer tipos para identificadores de la siguiente manera:


 type CarId = Flavor<number, “CarId”> type BoatId = Flavor<number, “BoatId”> 

Como la propiedad _type es opcional, puede usar una conversión implícita:


 let boatId: BoatId = 5; // OK let carId: CarId = 3; // OK 

Y todavía no podemos mezclar los identificadores:


 let carId: CarId = boatId; // ERROR 

Qué opción elegir


Ambas opciones tienen derecho a existir. La marca tiene la ventaja de proteger una variable de la asignación directa. Esto es útil si la variable almacena la cadena en algún formato, como una ruta absoluta de archivo, fecha o dirección IP. La función auxiliar que se ocupa de la conversión de tipos en este caso también puede verificar y procesar datos de entrada. En otros casos, es más conveniente usar Flavor.


Fuentes


  1. Punto de partida stackoverflow.com
  2. Interpretación gratuita del artículo.

Source: https://habr.com/ru/post/446768/


All Articles