TypeScript para desarrollo backend

El lenguaje Java aún reina supremo en el desarrollo de backend. Hay muchas razones para esto: velocidad, seguridad (si, por supuesto, cierra los ojos a los indicadores nulos), además de un ecosistema vasto y bien probado. Pero en la era de los microservicios y el desarrollo ágil, otros factores se han vuelto más importantes. En algunos sistemas, puede que no sea necesario mantener el máximo rendimiento y tener un ecosistema robusto de dependencias estables cuando se trata de un servicio simple que realiza operaciones CRUD y transformación de datos. Además, muchos sistemas deben construirse y reconstruirse rápidamente para seguir el ritmo del rápido desarrollo iterativo de funciones.



Es fácil desarrollar e implementar un servicio Java simple gracias a la magia dominante de Spring Boot. Pero debido a que las clases cerradas deben probarse y los datos deben transformarse, los constructores, los convertidores, los constructores de enumeraciones y los serializadores abundan en el código y allanan el camino para el infierno estereotipado de Java. Es por eso que el desarrollo de nuevas funciones a menudo se retrasa. Y sí, la generación de código funciona, pero no es muy flexible.



TypeScript aún tiene que establecerse bien entre los desarrolladores de backend. Probablemente porque se conoce como un conjunto de archivos declarativos que le permiten agregar algo de escritura a JavaScript. Pero aún así, hay una tonelada de lógica que requeriría docenas de líneas de Java para representar, y eso se puede representar en solo unas pocas líneas de TypeScript.

Muchas de las características que se dice que son características típicas de TypeScript en realidad se refieren a JavaScript. Pero TypeScript también se puede ver como un lenguaje propio, con algunas similitudes sintácticas y conceptuales con JavaScript. Así que vamos a apartarnos de JavaScript por un momento y echar un vistazo a TypeScript por sí solo: es un lenguaje hermoso con un sistema de tipos extremadamente poderoso pero flexible, toneladas de azúcar sintáctico y, finalmente, ¡seguridad nula!



Hemos alojado un repositorio en Github con una aplicación web Node / TypeScript personalizada, junto con algunas explicaciones adicionales. También hay una rama avanzada con un ejemplo de arquitectura de cebolla. y conceptos de mecanografía más no triviales.



Presentación de TypeScript



Comencemos con lo básico: TypeScript es un lenguaje de programación funcional asincrónico que, sin embargo, admite clases e interfaces, así como atributos públicos, privados y protegidos. Por lo tanto, el programador, cuando trabaja con este lenguaje, gana una flexibilidad considerable al trabajar a nivel de microarquitectura y estilo de código. El compilador de TypeScript se puede configurar dinámicamente, es decir, controlar qué tipos de importaciones están permitidas, si las funciones requieren tipos de retorno explícitos y si las comprobaciones de cero están habilitadas en tiempo de compilación.



Dado que TypeScript se compila en JavaScript normal, Node.js se utiliza como tiempo de ejecución de backend. En ausencia de un marco completo que se parezca a Spring, un servicio web típico usaría un marco más flexible que sirva como servidor web ( Express.js es un gran ejemplo de esto ). En consecuencia, resultará menos "mágico", y su escenario y configuración básicos se organizarán de forma más explícita. En este caso, los servicios relativamente complejos también requerirán más ajustes en la configuración. Por otro lado, configurar aplicaciones relativamente pequeñas no es difícil y, además, es factible casi sin estudiar primero el marco.



La administración de dependencias es fácil con el administrador de paquetes flexible pero poderoso de Node, npm.



Los basicos



Al definir clases public, los modificadores de control de acceso son compatibles protectedy private, bien conocidos por la mayoría de los desarrolladores:



class Order {

    private status: OrderStatus;

    constructor(public readonly id: string, isSpecialOrder: boolean) {
        [...]
    }
}


La clase ahora tiene Orderdos atributos: un statuscampo público y uno privado idde solo lectura. A máquina de escribir, argumentos de constructor con palabras clave public, protectedo privateatributos de clase se convierten automáticamente.



interface User {
    id?: string;
    name: string;
    t_registered: Date;
}

const user: User = { name: 'Bob', t_registered: new Date() };


Tenga en cuenta que, dado que TypeScript utiliza la inferencia de tipos, se puede crear una instancia del objeto User incluso si no se proporciona la clase en Usersí. Este enfoque similar a una estructura se elige a menudo cuando se trabaja con entidades de datos puras y no requiere ningún método o estado interno.



Los genéricos se expresan en TypeScript de la misma manera que en Java:



class Repository<T extends StoredEntity> {
    findOneById(id: string): T {
        [...]
    }
}


Potente sistema de tipos



En el corazón del poderoso sistema de tipos de TypeScript está la inferencia de tipos; también admite escritura estática. Sin embargo, las anotaciones de tipo estático son opcionales si el tipo de retorno o el tipo de parámetro se pueden inferir del contexto.



TypeScript también permite el uso de tipos de unión, tipos parciales e intersecciones de tipos, lo que le da al lenguaje una flexibilidad considerable al tiempo que evita una complejidad innecesaria. En TypeScript, también puede usar un valor específico como tipo, lo cual es increíblemente útil en una variedad de situaciones.



Enumeraciones, inferencia de tipos y tipos de unión



Considere una situación común en la que el estado del pedido debe tener una representación de tipo seguro (como una enumeración), pero también se requiere una representación de cadena para la serialización JSON. En Java, esto sería una enumeración, junto con un constructor y un captador para valores de cadena.



En el primer ejemplo, las enumeraciones de TypeScript le permiten agregar directamente una representación de cadena. Esto nos deja con una representación de enumeración segura de tipos que serializa automáticamente su representación de cadena asociada.



enum Status {
    ORDER_RECEIVED = 'order_received',
    PAYMENT_RECEIVED = 'payment_received',
    DELIVERED = 'delivered',
}

interface Order {
    status: Status;
}

const order: Order = { status: Status.ORDER_RECEIVED };


Observe la última línea de código, donde la inferencia de tipos nos permite crear una instancia de un objeto que coincide con la interfaz `Order`. Como no es necesario poner ningún estado interno o lógica en nuestro orden, podemos prescindir de clases y sin constructores.



Es cierto que resulta que al compartir la inferencia de tipos y tipos de unión entre sí, esta tarea se puede resolver aún más fácilmente:



interface Order {
    status: 'order_received' | 'payment_received' | 'delivered';
}

const orderA: Order = { status: 'order_received' }; // 
const orderB: Order = { status: 'new' }; //  


El compilador de TypeScript solo aceptará la cadena que se le proporcionó como un estado de pedido válido (tenga en cuenta que esto aún requerirá la validación del JSON entrante).



Básicamente, estas representaciones de tipos funcionan con cualquier cosa. Un tipo podría ser una unión de un literal de cadena, un número y cualquier otro tipo o interfaz personalizado. Para obtener ejemplos más interesantes, consulte la Guía de escritura avanzada de TypeScript .



Lambdas y argumentos funcionales



Dado que TypeScript es un lenguaje de programación funcional, tiene soporte para funciones anónimas, también llamadas lambdas, en su núcleo.



const evenNumbers = [ 1, 2, 3, 4, 5, 6 ].filter(i => i % 2 == 0);


El ejemplo anterior .filter()toma una función de tipo (a: T) => boolean. Esta función está representada por una lambda anónima i => i % 2 == 0. A diferencia de Java, donde los parámetros funcionales deben tener un tipo explícito, una interfaz funcional, el tipo lambda también se puede representar de forma anónima:



class OrderService {
    constructor(callback: (order: Order) => void) {
        [...]
    }
}


Programación asincrónica



Dado que TypeScript, con todas las salvedades, es un superconjunto de JavaScript, la programación asincrónica es un concepto clave en este lenguaje. Sí, puede usar lambdas y devoluciones de llamada aquí, TypeScript tiene dos mecanismos clave para ayudarlo a evitar el infierno de las devoluciones de llamada: promesas y un patrón bonito async/await. Una promesa es esencialmente un valor de retorno inmediato que promete devolver un valor específico más adelante.



//  ,  
function fetchUserProfiles(url: string): Promise<UserProfile[]> {
    [...]
}

//     
function getActiveProfiles(): Promise<UserProfile[]> {
    return fetchUserProfiles(URL)
        .then(profiles => profiles.filter(profile => profile.active))
        .catch(error => handleError(error));
}


Dado que las instrucciones .then()se pueden encadenar en cualquier número, en algunos casos el patrón anterior puede generar un código bastante confuso. Al declarar una función asyncy usarla awaitmientras espera que se resuelva la promesa, puede escribir este mismo código en un estilo mucho más sincrónico. También en este caso, se abre una oportunidad para utilizar operadores conocidos try/catch:



//  async/await ( ,  fetchUserProfiles  )
async function getActiveProfiles(): Promise<UserProfile[]> {
    const allProfiles = await fetchUserProfiles(URL);
    return allProfiles.filter(profile => profile.active);
}

//   try/catch
async function getActiveProfilesSafe(): Promise<UserProfile[]> {
    try {
        const allProfiles = await fetchUserProfiles(URL);
        return allProfiles.filter(profile => profile.active);
    } catch (error) {
        handleError(error);
        return [];
    }
}


Tenga en cuenta que aunque el código anterior parece ser sincrónico, solo es visible (ya que aquí se devuelve otra promesa).



Operador de extensión y operador de descanso: facilitando su vida



Cuando se usa Java, la manipulación de datos, la construcción, la fusión y la desestructuración de objetos a menudo producen código estereotipado en grandes cantidades. Hay que definir clases, generar constructores, captadores y definidores y crear instancias de objetos. En los casos de prueba, a menudo se requiere recurrir activamente a la reflexión sobre instancias simuladas de clases cerradas.



En TypeScript, todo esto se puede manejar sin esfuerzo aprovechando su dulce azúcar sintáctico seguro para tipos: operadores de propagación y operadores de descanso.



Primero, usemos el operador de expansión de matriz ... para descomprimir la matriz:



const a = [ 'a', 'b', 'c' ];
const b = [ 'd', 'e' ];

const result = [ ...a, ...b, 'f' ];
console.log(result);

// >> [ 'a', 'b', 'c', 'd', 'e', f' ]


Esto es conveniente, por supuesto, pero TypeScript real comienza cuando te das cuenta de que puedes hacer lo mismo con los objetos:



interface UserProfile {
    userId: string;
    name: string;
    email: string;
    lastUpdated?: Date;
}

interface UserProfileUpdate {
    name?: string;
    email?: string;
}

const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const update: UserProfileUpdate = { email: 'bob@example.com' };

const updated: UserProfile = { ...userProfile, ...update, lastUpdated: new Date() };

console.log(updated);

// >> { userId: 'abc', name: 'Bob', email: 'bob@example.com', lastUpdated: 2019-12-19T16:09:45.174Z}


Veamos qué está pasando aquí. Básicamente, un objeto updatedse crea utilizando el constructor de llaves. Dentro de este constructor, cada parámetro en realidad crea un nuevo objeto, comenzando por la izquierda.



Entonces, se usa el objeto extendido userProfile; lo primero que hace es copiarse a sí mismo. En el segundo paso, el objeto extendido se updatefusiona con él y se reasigna al primer objeto; esto, nuevamente, crea un nuevo objeto. En el último paso, el campo se fusiona y reasigna lastUpdated, luego se crea un nuevo objeto y, como resultado, el objeto final.



Usar el operador de propagación para crear copias de un objeto inmutable es una forma muy segura y rápida de procesar datos. Nota: El operador de propagación crea una copia superficial del objeto. Los elementos con una profundidad de más de uno se copian como enlaces.



El operador de extensión también tiene un destructor equivalente llamado resto de objeto :



const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const { userId, ...details } = userProfile;
console.log(userId);
console.log(details);

// >> 'abc'
// >> { name: 'Bob', email: 'bob@example.com' }


Ahora es el momento de simplemente sentarse e imaginar todo el código que tendría que estar escrito en Java para realizar las operaciones que se muestran arriba.



Conclusión. Un poco sobre las ventajas y desventajas.



Actuación



Dado que TypeScript es intrínsecamente asíncrono y tiene un entorno de ejecución rápido, existen muchos escenarios en los que un servicio Node / TypeScript puede competir con un servicio Java. Esta pila es especialmente buena para operaciones de E / S y funcionará bien con operaciones de bloqueo breves ocasionales, como cambiar el tamaño de una nueva imagen de perfil. Sin embargo, si el propósito principal de un servicio es realizar un cálculo serio en la CPU, Node y TypeScript probablemente no sean muy adecuados para esto.



Tipo de número



El tipo utilizado en TypeScript también deja mucho que desear number, que no distingue entre valores enteros y de punto flotante. La práctica demuestra que en muchas aplicaciones esto no presenta ningún problema. Sin embargo, es mejor no usar TypeScript si está escribiendo una aplicación para una cuenta bancaria o un servicio de pago.



Ecosistema



Dada la popularidad de Node.js, no debería sorprender que haya cientos de miles de paquetes en la actualidad. Pero como Node es más reciente que Java, muchos paquetes no han sobrevivido a tantas versiones y la calidad del código en algunas bibliotecas es claramente pobre.



Entre otras, vale la pena mencionar algunas bibliotecas de alta calidad con las que es muy conveniente trabajar: por ejemplo, para servidores web , inyección de dependencias y anotaciones de controlador . Pero, si el servicio dependerá seriamente de numerosos programas de terceros bien soportados, entonces es mejor usar Python, Java o Clojure.



Desarrollo acelerado de funciones



Como vimos anteriormente, una de las ventajas más importantes de TypeScript es lo fácil que es expresar lógica, conceptos y operaciones complejas en este lenguaje. El hecho de que JSON es una parte integral de este lenguaje, y hoy en día se usa ampliamente como formato de serialización de datos para la transferencia de datos y el trabajo con bases de datos orientadas a documentos, en tales situaciones parece natural recurrir a TypeScript. La configuración de un servidor Node es muy rápida, generalmente sin dependencias innecesarias; esto ahorrará recursos del sistema. Es por eso que la combinación de Node.js con el sólido sistema de tipos de TypeScript es tan eficaz para crear nuevas funciones en poco tiempo.



Finalmente, TypeScript está bien sazonado con azúcar sintáctico, por lo que el desarrollo con él es agradable y rápido.



All Articles