Complete Guide to Getting Started with TypeScript: Static Typing and Object-Oriented Programming

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 solutions to recurring design problems that are being applied daily in the software industry. Design patterns allow developers to have guidance in establishing the structure of a program, and making it more flexible and reusable.

What is Singleton?

Singleton is a design pattern that seeks to ensure that we can only instantiate a class once and verify that we are only instantiating a class once. Perhaps you've heard the controversy surrounding Singleton, it's even called an anti-pattern design. This is because the truth is that you will rarely use Singleton in your daily life as a programmer and the very definition of a design pattern is to solve everyday problems. Singleton is something you'll use for very particular situations. I encourage you not to use it, unless the situation requires it, but if you have the knowledge. Since all knowledge adds up. Let's see an example of what a class would look like, using this pattern.


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;
  }
}


What we see here is that we have a static variable (this is another type of accessibility, that is, we are talking about encapsulation). Specifically, static makes it usable anywhere) this instance variable is typed by the MyService class itself or null (that is, it is a union type) then there is a name variable that is string. Something very unique that we have in this class is a private constructor. Normally by definition in object orientation, constructors are public, but since we want them to only be instantiated once. It will be private, so you can't do the new XClase () (this is how you instantiate it, you'll see this in more depth) there is also a static method, that is, accessible anywhere, that receives a string per parameter and asks if the instance variable is equal to null, if so, we instantiate the class (here we are seeing how an instance is made) and finally we return the class with the instance variable.

Asynchronism and promises

What is asynchronism?

JavaScript is a language that can work both synchronously and asynchronously. Why do I mention JS, if this is a TS article? The good thing is that asynchronism occurs both in JS and in TS. I think that in JS you can better understand this concept. When does JS behave synchronously? JS behaves synchronously when all the programming depends on us, so it simply follows the thread of how the function is going to be executed. In the case of asynchronism, here it doesn't depend entirely on us, an example of this is the call to an API. The call depends on us, but how long it will take for that API to respond to us depends on the API and the server where we call. Therefore, this will always be asynchronous. Speaking of API calls, in TS this changes with respect to JS. In JS we used fetch, to call the API. In TS we'll install a library called axios (I think we can use fetch anyway, but usually axios is used)

npm i axios

After installing axios, in order to use an asynchronous context, it is done as follows:


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


Here we'll fake a delay for an asynchronous arrow function. That a number is entered as a parameter. A variable is created that is equal to an instance of promise (we'll see what this is) this is “promising to return a boolean” between another resolve parameter, we use the setTimeout function and this resolves to true. Taking the time it reaches us per parameter. We return the promise variable. This is a way of faking asynchrony, with a promise.

What is a promise?

When there is asynchronism and we are, for example, calling an external API that will take a while to respond, a promise is used in programming, which will be what we will return at the end of the code block. A promise is a special JS object that links the producer code (the code that calls the API) with the consumer code (when we consume the API and expect it to return something specific to us) an example of this:


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


Here again we see this interface. In this method, what we do is enter a creation DTO per parameter and we return a Promise object that in turn returns a product. When returning a product in a promise, we are inferring that this method is asynchronous and we will have to wait a while for it to respond

How do you use axios to consume APIS?

The first thing of course is to install Axios, which we've seen before. Then it would be importing axios.

Import axios from ‘axios’;

Then we must have a url (the one from the API) that usually goes in the constructor, then we use the reserved word await we use axios, followed by the verb that we will use in the method and by parameter we get the url and the necessary information. An example:


export class ProductHttpService implements ProductService

constructor(url: string) {}

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


Note that this would not work, because in the constructor we set a url variable that is typed as a string, the normal thing would be for us to enter the address of the API that we will consume as a string in the url. I did this just like that as an example. With the reserved word async we define that this will be an asynchronous function. We enter a creation DTO as a parameter and we define a variable that is equal to the reserved word await (we use this whenever we use something async) we use axios, the corresponding verb, here as we are creating, is post and by parameter we send the url and the creation DTO.

Generic classes

When we saw interfaces we said that they should not be confused with generic classes, well this is because generic classes are like the counterpart of an interface (nobody defines it that way, but that's how I see it) while an interface is typed to the parameter and typed to the return. But the method doesn't make sense and we give it to them when we implement it. Well, in generic classes it's just the opposite, they are untyped classes, methods whose parameters are not typed and their returns are not typed either. But this does make sense inside. Then, when we use these generic classes, we give them the respective types. These have the plasticity of being able to be any type, avoiding duplication of code when we change types. This is mostly used to create libraries, something we don't do much on a daily basis. But it is also used for the creation of repositories and that is, if it is very used (by the way, we will not see repositories because that escapes the objective of this course) basically stick to the concept of a generic class, but don't occupy them much, unless you need to create a repository. It's a case similar to that of Singleton, they're not things that we're going to be taking up a lot of. We can understand generics as a kind of “template” of code, through which we can apply a certain type of data to various points in our code. They serve to take advantage of code, without having to duplicate it due to type changes and avoiding the need to use the “any” type. An example of this:


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


As we can see in this method of updating, we get an id and a DTO, the id will be of type ID and the DTO of type DTO. What kind of typing is this, in this method being generic, we don't define it. But when we use this class, we will give it typing.

Decorators

In TypeScript. Decorator is a structural design pattern that allows you to dynamically add new behaviors to objects by placing them inside special objects that wrap them (_wrappers_). Using decorators, you can wrap objects countless times, since the target objects and the decorators follow the same interface. They help us to give some rules, so to speak, to variables. The first thing will be to install the decorators:

npm i class-validator –save

An example of how to use decorators would be:


export class CreateCategoryDTO implements ICreateCategoryDTO {

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

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

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


The first decorator we see is Lenght, whose parameter is 4 and 140. What this does is validate that our variable has a minimum of 4 characters and a maximum of 140. The next one is IsNotEmpty. What it does is verify that it does not come empty. Then IsUrl. This verifies that you have the regular expression of a url. We also have ISenum and by parameter we give the AcceType enum so it verifies that it is the enum that we give by parameter. Finally, the IsOptional decorator. With this we verify that this data, if it can come empty.

Bringing together everything learned

We have already reached the final part of this article where we have seen TS from its definition, to POO and other things. But what would the structure of a project be like using everything learned? I think you more or less already have this answer after reading everything. Even so, I'll leave you how this structure would go:

The first thing would be the model here will be what our structure of the database tables will be like:


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;
}



The next step will be to define the 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 {}


After having our models and DTO, the next thing will be the interface where we establish the CRUD skeleton.


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
}


After having the interface with the skeleton of what our CRUD would be like, it is time to do the service where we will implement this interface.


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;
} 
}



Conclusion

With this we have already concluded this article, where we saw everything about TS. Now you would be able to configure it, understand how typing works, arrays, union types, own data types, ENUM, DTO. Interfaces, which is one of the fundamentals of POO (polymorphism), common in Frameworks such as Nest where TypeScript is at the core, asynchronism and promise. Something that is also essential when programming, either in Node with any of its Frameworks, even on the front-end.

Ready to take your development skills to the next level with TypeScript?

At Kranio, we have experts who will help you integrate TypeScript into your projects, improving the quality and maintainability of your code. Contact us and discover how we can boost your professional development.

Team Kranio

September 16, 2024