Como empezar en TypeScript

En este artículo veremos que es TypeScript, cómo podemos usar este super set para qué JavaScript pase de un lenguaje de script, a un lenguaje fuertemente tipado, con objetos y todos los pilares de POO.

👀 Conocimientos requeridos: JavaScript básico

¿Qué es TypeScript?

TypeScript es un superset para JavaScript, este le da a JS un tipado fuerte, interfaces, clases, herencia y todo lo que tendría lenguajes como Java, C# etc..

Configuración inicial

Lo primero que se hará es desde la consola posicionarnos en la carpeta donde se estará trabajando e instalar TS, para esto se usará en la consola este comando:

npm i typescript --save-dev

Luego en la carpeta de trabajo, crearemos un archivo .gitignore. Esto sirve para obviar ciertos archivos que no queremos que vayan a nuestro repositorio en github. Para esto esta la pagina:

https://www.toptal.com/developers/gitignore

Aquí pondremos los tipos de archivos que queremos ignorar, estos tienen que ir separados de una coma. Generalmente se obvian los distintos sistemas operativos y Node.

Luego creas el archivo .editorconfig:

 
              #Editor configuration, see https://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.ts]
quote_type = single

[*.md]
max_line_length = off
trim_trailing_whitespace = false>

El siguiente paso es crear la carpeta src. Aquí es donde irán los archivos .ts (archivos TypeScript)

Luego vamos al archivo tsconfig.json, en donde la mayoría de líneas están comentadas, lo que se descomentara es outDir: "./dist",

de esta manera se configura que cualquier cosa que se compile, parta hacia la carpeta dist. Otro es rootDir: "./src", 

es decir, nuestro directorio principal donde estarán todos nuestros archivos typescript será src y luego al guardar, se hará la

transpilación automática a dist.

Ahora para que esto realmente funcione de forma automática, en la consola de nuestra carpeta ponemos npx tsc --watch y con esto la transpilación se hace de forma automática.

Luego para ejecutar un archivo pondremos lo siguiente en la consola:

node dist/archivo.js

Typado

Existe lo que es la inferencia de tipado:


let xNumero  = 1; --> esto lo tomara como un number
let xString = 'xCadena' --> esto lo tomara como un String

Esto es lo que hacemos normalmente en JS y también lo podemos hacer en TS.

Pero para tener una función con parámetros que vengan con un tipado explícito y un retorno también explícito (esto solo se puede hacer en TS)


const calcTotal = (prices: number[]): number => {
	let total = 0;
	prices.forEach((item)=>{
		total += item;
	});
	return total;
}

Esta función lo que hace es recibir un parámetro prices que es de tipo explícito un arreglo de number y retorna de forma explícita un number. El resto funciona como una función normal de JS

Arreglos

En los arreglos también se pueden inferir tipos de datos, así como darlos explícitos. Estos arreglos pueden ser de un tipo de datos, o de varios. Por ejemplo:

const prices = [1,2,3,4];

Esto es un arreglo implícito de number, esto también es así en JS. 

const mixed = [1,2,'hola',true]

Este es un arreglo mixto, en donde el tipado quedaría (number | string | boolean)[] Esta inferencia también la podemos ver en JS. Luego tenemos un arreglo mixto explícito:

const mixed: (number | string | boolean)[]

Esto ya es propio de TS en donde primero debe ir number, luego string y finalmente boolean

Union types

Los union types, son variables que pueden tener varios tipados. Por ejemplo, una variable que sea de tipo string, o boolean, o number. Es decir, tiene una mayor flexibilidad de tipos. En JS cuando declaramos una variable, si no le damos un valor, esta será de tipo any, no toma un valor hasta que se lo asignemos, e incluso ese valor podría cambiar. En TS esto no funciona igual (a menos que le asignemos el tipo any a una variable, pero perderíamos toda la potencia de TS) en TS las variables si tienen un tipado, en el caso de los union types es como un tipado propio, que puede ser varios. Por ejemplo:

let userId: string | number;

Esto quiere decir que userId puede ser un string o un number y como esta es un let, si en el futuro cambia de valor, este valor puede ser un string, o un number. Pero si le damos como valor un boolean, esto nos arrojará un error.

Tipos de dato propios

Con los union types dijimos que eran como un tipado propio. Pero ojo en él como un. Porque union types y tipado propio no es lo mismo. Con un ejemplo quedará más claras sus diferencias:

type XDatoPropio = string | number | boolean

Como vemos es muy parecido al union types. Pero aquí tiene la palabra reservada type. Por lo que, si declaramos una variable, por ejemplo, estado. Quedaría así:

const estado: XDatoPropio;

Al hacer esto, estado podría tomar como valor, un string, un number o un boolean.

También tenemos los tipos de dato literales. Un ejemplo de esto sería:

type Sizes = 'XS' | 'S' | 'M' | 'L' | 'XL';

Es decir que, si tengo la variable talla y le doy el tipado Sizes, esta variable solo puede tener de valor una cadena que sea, XS, S, M, L, XL. Cualquier otro valor, arrojaría un error.

Por otro lado tenemos los tipos complejos, que actúan casi como una interfaz (pero no es lo mismo) un ejemplo seria:


type Product = {
	title: string,
	createdAt: Date,
	stock: number,
	size?: Sizes
}


Esto es bien interesante el tipo Product, tiene varias variables y cada una con su propio tipado, incluso tenemos una variable bastante particular como size, que tiene un signo de ? lo cual significa que es opcional, además este size es de tipo Size. Si recordamos, este tipo solo puede tener valores ya predeterminados. Por lo tanto, si una variable tiene de tipo Product, es casi como una interfaz que recibe ciertos valores:

const products: Product[] = [];

Esta variable products, tiene de tipado Product cómo arreglo, por lo que recibirá un title que es un string, un createdAt que es una fecha, un stock, que es un number y si es que se quiere un size, que es un Sizes. Pero esta última es opcional. Pero esto va más allá, podríamos tener funciones que nos den todo lo que tiene un CRUD, pero se almacenará en memoria y no en una BD.


const addProduct = (data: Product) => {
	products.push(data)
}


Aquí tenemos una función de flecha que sirve para agregar un producto al arreglo de productos. Por eso es que se le llama tipos complejos.

Programación modular

La programación modular es una forma de programar donde se divide nuestros archivos en carpetas. Por ejemplo, una manera de hacer esto, es tener una carpeta para el model, otra para el service y un archivo en la raíz que será el main. El model tendremos nuestros tipos propios y tipos complejos, en service tendremos las funciones y en el main, ejecutaremos esas funciones (esta es una forma de ver la programación modular, en realidad hay muchas. Pero su base es dividir nuestros archivos con una lógica que sustenta esta división)

Por ejemplo en nuestra carpeta model, tenemos un archivo products.model.ts (no es 100% necesario poner el .model, pero es una forma de hacer referencia que esto es un model y una buena práctica)


export type Sizes = 'XS' | 'S' | 'M' | 'L' | 'XL';
export type Product = {
			title: string,
			createdAt: Date,
			stock: number,
			size?: Sizes
		      };


Lo primero que se aprecia es la palabra reservada export. Esto se hace porque al programar de forma modular, nuestras otras carpetas deben compartir archivos y para eso necesitan poder ser exportados. En service iría esto:


import { Product } from './product.model';

export const products: Product[] = [];

export const addProduct = (data:Product) => {
	products.push(data);
}


Aquí importamos Product, es decir estamos exportando el tipo complejo Product, que tiene dentro el tipo Sizes. Además tenemos un arreglo de productos y la función que agrega un producto a ese arreglo.

Finalmente, el archivo main, seria asi:


import { addProduct, products } from './product.service'

addProduct({
	title: 'Pro1',
	createdAt: new Date(1991,4,6),
	stock: 12
});


Lo que aquí estamos importando es el servicio, que tiene el método agregar producto y el arreglo de productos. Finalmente ejecutamos el método addProduct dando los datos que tendrá.

Librerías con soporte para TypeScript

¿Qué es una librería?

Una librería es un conjunto de clases, interfaces y funciones bien definidas y listas para ser utilizadas. Cuando hablamos de librerías con soporte para TS, hablamos de librerías que se pueden usar tanto en JS, como TS.

Un ejemplo de librería con soporte para TS es date-fns:

Primero instalamos esta librería por lo que en la consola nos ubicamos en la raíz de nuestro proyecto y ejecutamos este comando en la consola:

npm i date-fns –save

Para poder utilizarla debemos importarla en el archivo que queramos utilizar:

import {subDays, format} from 'date-fns';

ejemplos de cosas  que podemos hacer con esta librería:


const birthday = new Date(1991, 4, 6);
const rta = subDays(birthday, 30);
const str = format(rta, 'yyyy'/MM/dd);


La primera variable es un Date con el año, mes, día. La variable rta, le está restando 30 dias a birthday. La variable str formatea la fecha en año/mes/día. Aunque podríamos formatearlo en día/mes/año si quisiéramos, o incluso de otra forma. En conclusión, lo que hace la librería date-fns es manipular fechas.

Librerías sin soporte para TypeScript

Cuando hablamos de librerías que no tienen soporte para TS, hablamos de librerías que no tienen un sistema de tipado. Por lo que tendremos que instalarlo. Un ejemplo de librería sin soporte para TS es lodash. Lo primero que haremos para utilizarla es instalarla:

npm i lodash

Ahora nos toca darle tipado a esta librería, para esto instalaremos lo siguiente:

Luego importamos esta librería donde queremos usarla:

import _ from 'lodash';

La librería lodash se usa para simplificar el manejo y edición de objetos, arrays, etc. ya que este proporciona muchos métodos de utilidad para hacerlo. Ejemplos:


const data = [
	{
		username: 'Gastón Fuentes',
		role: 'Admin'
	},
	{
		username: 'Andres Mazuela',
		role: 'seller'
	},
	{
		username: 'Paola Mazuela',
		role: 'seller'
	},
	{
		username: 'Andrew Vasquez',
		role: 'customer'
	}
];


Aquí tenemos un arreglo, que adentro tiene JSON. Con lodash podríamos por ejemplo agrupar estos JSON por roles:

const rta = _.groupBy(data, (item)=>item.role);

Al hacer esto quedaría un JSON de la siguiente manera:


{
	admin: [  { username: 'Gastón Fuentnes', role: 'admin' }  ]
	seller: [
		  {username: 'Andres Mazuela', role: 'seller'}
		  {username: 'Paola Mazuela', role: 'seller'}
		],
	customer: [  { username: 'Andrew Vazques', role: 'customer' }  ]
}


ENUMS

Los enums son como un tipo de dato propio, pero en donde cada variable, tiene su propia opción y esta debe estar en mayúscula. Ejemplo:


enum ROLES {
  ADMIN = "admin",
  SELLER = "seller",
  COSTUMER = "costumer",
}


Lo primero que aquí se nota a diferencia de un tipo propio es la palabra reservada enum seguido del tipo ROLES, todo en mayúscula. Luego estan las opciones que están todas en mayúscula, con su valor respectivo. Cuando se usan enum solo se puede dar el valor que definimos acá, no se puede dar ningún otro valor. Ejemplo:

role: ROLES;

La variable role, solo tomara como valor, admin, seller, o costumer.

Interface

Las interfaces se asemejan a los tipos propios, pero tienen sus diferencias sustanciales. Un ejemplo de interfaz es:


interface Product {
  productId: string | number;
  productTitle: string
  productCreateAt: Date;
  productStock: number;
  productSize?: Sizes;
}



Lo primero que se nota es que se usa la palabra reservada interface, además esta interfaz de Product se debe cumplir al pie de la letra. Puesto que una interfaz, es como un contrato, en donde todo lo que está estipulado, debe cumplirse.

Otra característica que tienen las interfaces, es que se puede heredar. Un ejemplo de esto:



export interface BaseModel {
  readonly id: string | number;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}


Aquí se utiliza la palabra reservada readonly, esto significa que esta variable es de solo lectura. Por lo que, si queremos hacer cambios con esas variables, no podremos. Esta es una clase padre, que tiene cosas que tienen la mayoría de interfaces, como es un id, una fecha de creación y una fecha de actualización. Para heredar esto hacemos lo siguiente.


export interface Product extends BaseModel {
  productTitle: string;
  productPrice: number;
  productImage: string;
  productSize: Sizes;
  productStock: number;
  productDescription?: string;
  productCategory?: Category;
  productIsNew?: boolean;
  productTags?: string[];
}


Al poner la palabra extends heredamos la interfaz BaseModel, con todas sus variables.

También se puede tener métodos en las interfaces, por ejemplo, se puede tener una interfaz donde vaya todo un CRUD, pero aquí no irá la lógica, sólo irá la estructura de ese método, con sus parámetros, y lo que retornara. Cuando implementemos una interfaz, recordemos que esta tiene que cumplirse al pie de la letra, por lo que si hacemos un método como una interfaz, tenemos que cumplir su estructura, pero la lógica la damos nosotros. Un ejemplo de esto:


export interface ProductService {
	createProduct(createDTO: CreateProductDTO): Product
	findProducts(): Product[]
	findProduct(id: Product[‘id’]): Product
	updateProduct(id: Product[‘id’], updateDTO: UpdateProductDTO): Product
}



Esto será complicado de entender en un principio, más que nada por los DTO, pero es algo que se vera más tarde. En el método de crear un producto recibimos un DTO de creación como parámetro y retornamos un producto, luego vamos haciendo las estructuras de los otros métodos del CRUD. Para implementar una interfaz. Se hace así:


Import { ProductService } from ‘../models/producto-service.model’;
export class ProductMemoryService implements ProductsService {}


Lo primero es importar la interfaz, luego se implementa con la palabra reservada implements, aquí también vemos class. Pero es algo que se verá más adelante.

DTO

Los DTO o data transfer object. Sirven para facilitar la comunicación entre sistemas. Por ejemplo, tenemos un DTO para crear y otro para actualizar. Por ejemplo, para crear:


export interface CreateProductDTO 
extends Omit Product 'id' | 'createdAt' | 'updatedAt' | ' productCategory' {
  categoryId: string;
}


Este DTO para crear un producto hereda de un Omit (un Omit es un utility type) en donde omitiremos las variables id, fecha de creación, fecha de actualización y la categoría del producto. Por lo demás recibimos todas las otras variables del producto más una id de categoría. Este es el potencial de un utility type que nos permite que en nuestro DTO no escribamos mucho código.

Para crear un DTO de actualización, se haría así:

export interface UpdateProductDTO extends Partial CreateProductDTO {}

Aquí nuevamente heredamos un utility type, esta vez Partial. Es decir que todas las variables del DTO de crear un producto serán opcionales (al ser del DTO de crear un producto, va con el Omit incluido) nuevamente vemos el potencial de los utility type, que nos ahorra escribir mucho código, así como reutilizar nuestro DTO de creación.

POO

¿Qué es POO?

POO o programación orientada a objeto, es un paradigma de programación que usa objetos y sus interacciones, para diseñar aplicaciones y programas informáticos. Está basado en técnicas, incluyendo herencia, abstracción, polimorfismo y encapsulamiento. Los objetos pueden ser como los que vemos en la vida cotidiana, o incluso personas, animales. Normalmente los usamos para representar modelos, mapeos etc.. aunque estos conceptos pueden sonar raros por el momento. Un ejemplo de un objeto sería este:


export abstract class Animal {
	protected name: string;

	constructor(name: string){
		this.name = name;
}

get move(): string {
	return ‘moving along!!’;
}

get greeting(): string {
return `Hello i´m ${this.name}`;
}
}


Aquí hay una clase abstracta (generalmente las clases abstractas se usan para ser heredadas) en donde tenemos la accesibilidad de la variable. Protected se utiliza en las clases abstractas y sólo pueden ser accedidas estas variables cuando se heredan, otra accesibilidad sería private, en este caso la variable solo puede ser utilizada dentro de la clase y utilizando this. Luego están la accesibilidad public, esta puede ser utilizada dentro de la clase o de otra si la importamos y la instanciamos (luego se verán estas cosas) por defecto si no ponemos nada, la accesibilidad de una variable o método es público. Luego vemos el constructor, esto debe ir siempre en una clase y es donde entran las variables de la clase. Por ejemplo, el name, definimos un name como el que tenemos como variable. Luego hacemos this.name = name (el this.name es la variable protected de la clase y el name es la variable definida en el constructor) luego se tienen métodos get que retorna un string. Un método get, es un método que se comporta como una variable de la clase para que esto funcione debe ir como get xNombreMetodo y no puede recibir parámetros, puede retornar algo, así como ser un void (que no retorna nada)

Ya con esto hemos visto tres conceptos que componen la orientación a objeto. El encapsulamiento (que es la accesibilidad de las variables y métodos. Ósea private, public, protected) recordemos que protected se usa en clases abstractas y con eso estamos llegando a otro concepto que es la abstracción. La abstracción es una clase que tiene variables y métodos que ocupan muchas otras clases (no confundir con clases genéricas) y con esto llegamos al tercer concepto que es la herencia. Que básicamente es traspasarlo a una clase la información de su “padre” que tiene esa información que utilizará la clase y a su vez también otras clases. Pero nos falta aún un concepto más, que veremos ahora.

Polimorfismo

Técnicamente el polimorfismo ya lo hemos visto, puesto que esto hace referencia a las interfaces y es que las interfaces recordemos que son como un contrato que tiene el esqueleto ya sea de un modelo, de un método (en donde definimos los parámetros de este y su retorno) pero la lógica se le da una vez que esta es implementada en alguna clase. Un ejemplo real de cómo sería una interface y como la implementaríamos en una clase (digo ejemplo “real” porque es básicamente como funciona, pero resumido solo para dar el ejemplo)


export interface IDriver {
  database: string;
  password: string;
  port: number;

  connect(): void;

  disconnect(): void;

  isConnected(name: string): boolean;
}



Esta interfaz llamada IDriver, que es una interfaz muy utilizada para ORM (no importa si esto no se entiende, lo importante es quedarse con el concepto) básicamente tenemos una interface que tiene como variables el nombre de una BD, una contraseña y un puerto, así como un método para conectar, que retorna un void, otro para desconectar, que también retorna un void y uno que es para ver si está conectada la BD, que recibe un nombre que es un string y retorna un boolean. Osea si implementamos esta interface, tenemos que cumplir con todo lo que esta nos pide. Un ejemplo de una clase que la implementa es esta:


export class PostgresDriver implements IDriver {
  constructor(
    public database: string,
    public password: string,
    public port: number
  ) {}

  connect(): void {}

  disconnect(): void {}

  isConnected(name: string): boolean {
    return true;
  }
}



Esta es una forma resumida de cómo funciona la librería de la ORM de Postgres, en donde implementamos IDriver. En el constructor tenemos las variables de la interfaz, luego tenemos los métodos. Así implementamos todo lo que nos pide la interfaz. A esto le llamamos polimorfismo. Ya con esto hemos visto los 4 pilares de POO o programación orientada a objeto.

Patrón de diseño Singleton

¿Qué es un patrón de diseño?

Los Patrones de diseño en programación son soluciones a problemas recurrentes de diseño y que se están aplicando a diario en la industria del software. Los patrones de diseño permiten a los desarrolladores tener una guía a la hora de establecer la estructura de un programa, y hacerlo más flexible y reusable.

¿Qué es Singleton?

Singleton es un patrón de diseño que busca que solo podamos instanciar una vez una clase y comprobar que solo estemos instanciando una clase una vez. Quizás has escuchado la polémica sobre Singleton, incluso se la llama antipatrón de diseño. Esto porque la verdad es que muy rara vez vas a usar Singleton en tu día a día como programador y la definición misma de un patrón de diseño es solucionar problemas del día a día. Singleton es algo que usarás para situaciones muy particulares. Te animo a no utilizarlo, a menos que la situación lo requiera, pero si a que tengas el conocimiento. Ya que todo conocimiento suma. Veamos un ejemplo de cómo sería una clase, utilizando este patrón.


export class MyService{
  private name: string;
  static instance: MyService | null = null;

  private constructor(name: string){
    this.name = name;
  }

  static create(name: string){
    if(MyService.instance === null){
      MyService.instance = new MyService(name);
    }
    return MyService.instance;
  }
}


Lo que vemos aquí es que tenemos una variable static (este es otro tipo de accesibilidad, es decir, hablamos de encapsulamiento. Concretamente static hace que pueda ser utilizada en cualquier lugar) esta variable instance tiene como tipado la propia clase MyService o null (es decir es un union type) luego hay una variable name que es string. Algo muy particular que tenemos en esta clase es un constructor privado. Normalmente por definición en orientación a objeto, los constructores son públicos, pero como queremos que solo se instancie una vez. Será privado, así no se podrá hacer el new xClase() (así es como se instancia, ya se verá más a profundidad esto) también hay un método estático, es decir accesible en cualquier lugar, que recibe un string por parámetro y pregunta si la variable instance es igual a null, de ser así, instanciamos la clase (aquí estamos viendo como se hace una instancia) y finalmente retornamos la clase con la variable instance.

Asincronismo y promesas

¿Qué es el asincronismo?

JavaScript es un lenguaje que puede funcionar tanto de forma síncrona, como asíncrona. ¿Por qué menciono a JS, si este es un artículo de TS? Bueno es que el asincronismo se presenta tanto en JS, como en TS. Creo que en JS se puede entender mejor este concepto. ¿Cuándo JS se comporta de forma síncrona? JS se comporta de forma síncrona cuando toda la programación depende de nosotros, por ende, simplemente sigue el hilo de cómo se va a ejecutar la función. En el caso del asincronismo, aquí no depende enteramente de nosotros, un ejemplo de esto es el llamado a una API. El llamado depende de nosotros, pero lo que va a tardar esa API en respondernos, depende de la API y el servidor donde llamamos. Por ende, esto siempre será asíncrono. Hablando de llamados a API, en TS esto cambia con respecto a JS. En JS utilizábamos fetch, para llamar a API. En TS instalaremos una librería llamada axios (creo que de todas maneras podemos usar fetch, pero por lo general se usa axios)

npm i axios

Luego de instalar axios, para poder utilizar un contexto asíncrono, se hace de la siguiente manera:


(async () => {
  function delay(time: number) {
    const promise = new Promise boolean ((resolve) => {
      setTimeout(() => {
        resolve(true);
      }, time);
    });
    return promise;
  }
})();


Aquí fingiremos un delay para una función de flecha asíncrona. Que le entra un number como parámetro. Se crea una variable que es igual a una instancia de promesa (ya veremos que es esto) esto está “prometiendo que retornara un boolean” entra otro parámetro resolve, usamos la función setTimeOut y esta se resuelve como true. Tardando el tiempo que nos llega por parámetro. Retornamos la variable de la promesa. Esta es una forma de fingir una asincronía, con una promesa.

¿Qué es una promesa?

Cuando existe asincronismo y estamos, por ejemplo, llamando una API externa que tardará un tiempo en responder, en programación se utiliza una promesa, que será lo que retornaremos al final del bloque de código. Una promesa es un objeto especial de JS que une el código productor (el código que llama a la API) con el código consumidor (cuando consumimos la API y esperamos que esta nos devuelva algo específico) un ejemplo de esto:


export interface ProductService {
	createProduct(createDTO: CreateProductDTO): Promise Product
}


Aquí nuevamente vemos esta interface. En este método lo que hacemos es que nos entra un DTO de creación por parámetro y retornamos un objeto de Promise que a su vez retorna un producto.  Al retornar un producto en una promesa, estamos infiriendo que este método es asíncrono y tendremos que esperar un tiempo en que responda

¿Cómo se utiliza axios para consumir APIS?

Lo primero por supuesto es instalar axios, cosa que ya vimos anteriormente. Luego sería importar axios.

Import axios from ‘axios’;

Luego debemos tener una url (la de la API) está por lo general va en el constructor, luego usamos la palabra reservada await usamos axios, seguido del verbo que utilizaremos en el método y por parámetro nos llega la url y la información necesaria. Un ejemplo:


export class ProductHttpService implements ProductService

constructor(url: string) {}

async createProduct(CreateDTO: CreateProductDTO) {
	const { data } = await axios.post(this.url, CreateProductDTO);
	return data;
}


Ojo que esto no funcionaria, porque en el constructor establecemos una variable url que tiene de tipado string, lo normal sería que en la url pusiéramos la dirección de la API que consumiremos como una cadena. Esto lo hice así nada más como ejemplo. Con la palabra reservada async definimos que esta será una función asíncrona. Nos entra un DTO de creación como parámetro y definimos una variable que es igual a la palabra reservada await (esto lo utilizamos siempre que usemos algo async) usamos axios, el verbo correspondiente, aquí como estamos creando, es post y por parámetro mandamos la url y el DTO de creación.

Clases genéricas

Cuando vimos interfaces dijimos que no había que confundirlas con las clases genéricas, bueno esto es porque las clases genéricas es como la contraparte de una interfaz (nadie lo define así, pero es como lo veo yo) mientras una interfaz se le da tipado al parámetro y un tipado al retorno. Pero no tiene lógica el método y se la damos cuando la implementamos. Bueno en las clases genéricas es justo lo contrario, son clases sin tipado, métodos cuyos parámetros no tienen tipado y sus retornos tampoco lo tienen. Pero esto sí tiene lógica dentro. Luego cuando utilizamos estas clases genéricas le damos los tipados respectivos. Estas tienen una plasticidad de poder ser cualquier tipo, evitando duplicidad de código cuando cambiamos tipados. Esto se usa más que nada para crear librerías, algo que no hacemos mucho día a día. Pero también se utiliza para la creación de repositorios y eso si es muy utilizado (por cierto, no veremos repositorios porque eso escapa al objetivo de este curso) básicamente quédense con el concepto de una clase genérica, pero no las ocupen mucho, a menos que necesiten crear un repositorio. Es un caso similar al de singleton, no son cosas que vamos a estar ocupando mucho. Podemos entender los genéricos como una especie de "plantilla" de código, mediante la cual podemos aplicar un tipo de datos determinado a varios puntos de nuestro código. Sirven para aprovechar código, sin tener que duplicarlo por causa de cambios de tipo y evitando la necesidad de usar el tipo "any". Un ejemplo de esto:


async update ID, DTO (id: ID, updateDTO: DTO) {
    const { data } = await axios.put(`${this.url}/${id}`, updateDTO);
    return data;
  }


Como vemos en este método de actualizar nos llega un id y un DTO, el id será de tipo ID y el DTO de tipo DTO. Que clase de tipado es este, en este método al ser genérico, no lo definimos. Pero cuando utilicemos esta clase, si le daremos tipado.

Decoradores

En TypeScript. Decorator es un patrón de diseño estructural que permite añadir dinámicamente nuevos comportamientos a objetos colocándolos dentro de objetos especiales que los envuelven (_wrappers_). Utilizando decoradores se puede envolver objetos innumerables veces, ya que los objetos objetivo y los decoradores siguen la misma interfaz. Nos sirven para darle unas reglas por decirlo así a variables. Lo primero será instalar los decoradores:

npm i class-validator –save

Un ejemplo de cómo usar decoradores seria:


export class CreateCategoryDTO implements ICreateCategoryDTO {

  @Length(4, 140)
  @IsNotEmpty()
  name!: string;

  @IsUrl()
  @IsNotEmpty()
  image!: string;

  @IsEnum(AccesType)
  @IsOptional()
  acces?: AccesType | undefined;
}


El primer decorador que vemos es Lenght que le llega por parámetro un 4 y 140. Esto lo que hace es validar que nuestra variable tenga un mínimo de 4 caracteres y un máximo de 140. El siguiente es IsNotEmpty. Esto lo que hace es verificar que no venga vacío. Luego IsUrl. Esto verifica que tenga la expresión regular de una url. También tenemos IsEnum y por parámetro damos el enum AccesType de esta forma verifica que sea el enum que damos por parámetro. Finalmente, el decorador IsOptional. Con esto verificamos que ese dato, si puede venir vacío.

Unir todo lo aprendido

Ya hemos llegado a la parte final de este artículo en donde hemos visto TS desde su definición, hasta POO y demás cosas. ¿pero cómo sería la estructura de un proyecto usando todo lo aprendido? Creo que esta respuesta más o menos ya la tienes después de haber leído todo. Aun así, te dejare como iría esta estructura:

Lo primero sería el modelo aquí irá el cómo será nuestra estructura de las tablas de la BD:


import { Category } from ‘./category.model’;

export interface Product{
	id: number;
	
	@Lenght(4, 140)
	@IsNotEmpty()
	title: string;

	@IsNotEmpty()
	price: number;

	@Length(4, 250)
	@IsOptional()
	description: string;
	
	category: Category;

	@IsUrl()
	@IsNotEmpty()
	Image: string[];
	
	categoryId: number;
}



El siguiente paso será definir el DTO:


import { Category } from ‘../models/category.model’;
import { Product } from ‘../models/product.model’;

export interface CreateProductDTO extends Omit Product, ‘id’ | ‘category’ {
	categoryId: Category[‘id’];
}

export interface UpdateProductDTO extends Partial CreateProductDTO {}


Luego de tener nuestros modelos y DTO, lo siguiente será la interface donde establecemos el esqueleto del CRUD.


import { CreateProdcutDTO, UpdateProductDTO } from ‘../dtos/producto.dto’;
import { Prodcut } from ‘./producto.model’;

export interface ProductService {
	CreateProdcut(createDTO: CreateProductDTO): Promise Product ;
	FindProducts(): Promise Product[] ;
	FindProduct(id: Product[‘id’]): Promise Product ;
	UpdateProduct(id: Prdocuct[‘id’], updateDTO: UpdateProductDTO): Promise Product
}


Luego de ya tener la interface con el esqueleto de como seria nuestro CRUD toca hacer el servicio donde implementaremos esta interfaz.


import axios from ‘axios’;
import { CreateProdcutDTO, UpdateProductDTO } from ‘../dtos/producto.dto’;
import { ProductService } from ‘../models/producto-service.model’;
import { product } from ‘../models/producto.model’;

export class ProductHttpService implements ProductService {
	constructor(url: string) {}

	async createProduct(createDTO: CreateProductDTO) {
		const { data } = await axios.post(this.url, createDTO);
		return data;
}

async findProducts() {
	const { data } = await axios.get Product[] (this.url);
	return data;
}

async findProduct(id: Product[‘id’]) {
	const { data } = await axios.get Product (`${this.url}/${id}`);
	return data;
}

async updateProduct(id: Product[‘id’], updateDTO: UpdateProductDTO) {
	const { data } = await axios.put(`${this.url}/${id}`, updateDTO);
	return data;
} 
}



Conclusión

Ya con esto hemos dado por concluido este artículo, en donde vimos todo acerca de TS. Ahora serías capaz de configurarlo, entender cómo funciona el typado, arreglos, union types, tipos de dato propios, ENUM, DTO. Interfaces, lo cual es uno de los fundamentos de POO (polimorfismo), común en Frameworks como Nest donde su núcleo es TypeScript, asincronismo y promesa. Algo que también es fundamental a la hora de programar, ya sea en Node con cualquiera de sus Frameworks, incluso en el front-end.

📢 Recuerda seguirnos en linkedln para que conozcas más de cerca al equipo kranio y no te pierdas de contenido exclusivo para ti.

Gaston Fuentes

September 16, 2024