imagen

Primeros pasos con TypeScript

Instalación

Global: npm install -g typescript, nos permitirá usarlo en cualquier proyecto, en nuestra maquina local. Local: npm install typescript --save-dev, no depende de que la maquina local del desarrollador tenga ts instalado, se instalara con todas las demás dependencias de desarrollo (recomendada).

Para verificar la version instalada
Global: tsc -v Local: npm ls typescript (o mirando el package.json)

Configuración inicial

tsc -init creara el archivo tsconfig.json, donde definiremos el comportamiento que queremos de nuestro compilador de .ts.

Pondremos especial atención en las siguientes configuraciones:

"target":"ES2016": nos permite especificar la version
"rootdir":"./src": indica donde estarán los archivos de origen de nuestra aplicación
"outDir":"./dist": en que carpeta sera generado el archivo .js
"noEmitOnError":false: hara que falle la compilación al encontrar errores
"removeComments":true: removerá los comentarios en los archivos de salida
"sourceMap":true: genera el mapeo de los archivos .ts a .js

Con la configuración anterior, solo bastara con ejecutar tsc en la linea de comando para que typescript ejecute el compilador usando las configuraciones definidas en tsconfig.json

Tipos

JS nativos

  • number
  • string
  • boolean
  • null
  • undefined
  • object
  • function

TS

  • any : no se recomienda usarlo, solo para proyectos que se estan migrando a TS
  • unknown : Tipo desconocido
  • never
  • arrays
  • tuplas
  • Enums

Numbers, strings y boolean

/* inicializadas, TS infiere el tipo de dato y es opcional indicarlo */
let monto = 340_000
let nombre = "Juan"
let pagado = false

/* no inicializadas, es necesario indicar tipo */
let monto: number;
let nombre: string

/* o puede indicarse el tipo y ademas inicializarlas */
let monto: number = 340_000
let nombre: string = "Juan"
let pagado: boolean = false

function pagar(monto: number){
}

Arrays

/* Arrays */
let calificaciones: number[] = [45,67,99]
let calificaciones: Array<number> = [45,67,99]

let zones: string[] = ["london","new york","barcelona"]
let zones: Array<string> = ["london","new york","barcelona"]

/* Array mixtos */
let mixto: (number | string | boolean | Object) = ["hola",333, true]

Tuplas

Similares a los array, pero con longitud fija y con tipos de datos definidos.

/* tuplas */
let dato1:[number,string] = [1, "london"]
let dato2:[number,string[]] = [1, ["london","cali"]]

/* Al tener estructura fija, facilita la destructuración */
const [id, city] = dato1

Unknow

A diferencia de any usar unknow nos obliga a verificar el tipo de variable antes de operar con la variable de tipo unknow

let unknowVar: unknow;
unknowVar = true
unknowVar = 444
unknowVar = "hola"

// No nos permite hacer esto
unknowVar.toUpperCase()

// Nos obliga a hacer esto
if(typeof unknowVar === "string){
   unknowVar.toUpperCase()
}

Never type

Lo usamos para indicar que una función nunca va a retornar algo, su ejecución no termina, un ejemplo practico es cuando se levanta una excepción del tipo throw, que detiene la ejecución a causa de un error.

const fail = (message: string): never =>{
  throw new Error(message)
}

Enums

/* enums : básicamente son diccionarios */
/* Ejemplo 1 */
enum Talla { 
  Chica = "s", 
  Mediana = "m",
  Grande = "l",
  ExtraGrande = "xl" }

const variable = Talla.Chica // "s"

const enum LoadingState { Idle, Loading, Success, Error}
const estado = LoadingState.Success // 2

/* Ejemplo 2 */
enum Roles {
  admin: "administrator",
  seller: "seller",
  customer: "customer",
}

type User = {
  userName: string
  role: Roles
}

const newUser: User = {
  userName: "Jhon",
  role: Roles.admin // "administrator"
}

Union types

/* puede ser un string o number */
let userId: string | number
userId: 666
userId: "BG767"

function greeting(myText: string | number){
// your code
}

Objetos

/* Objetos */
const reserva: {
  id: number,
  nombre?: string, //? indica que el atributo es opcional
  readonly value: number // atributo de sólo lectura
  talla: Talla // tipo enums
} = {
  id: 1,
  nombre: "p34",
  value: 800_000
  talla: Talla.Chica
}

Tipos personalizados

/* tipos personalizados */
type Reserva = {
  id: number,
  nombre?: string, //? indica que el atributo es opcional
  readonly value: number // atributo de sólo lectura
  talla: Talla // tipo enums
}

const reserva: Reserva = {
  id: 1,
  nombre: "p34",
  value: 800_000
  talla: Talla.Chica
}

/* Array de tipos personalizados */
const reservas: Reserva[]=[]

/* Tambien lo podemos usar con union type */
type UserId = string | number

Literal types

/* Permitira solo los string que se le indiquen */
let shirtSize: "s" | "m" | "l" | "xl"
let shirtSize = "s"

/* lo recomendable seria definirlo como tipo personalizado */
type Sizes = "s" | "m" | "l" | "xl"
let shirtSize: Size
let shirtSize = "s"

Null y undefined

/* TS reconocera estas variables como Any */
let myNull = null
let myUndefined = undefined

/* la forma correcta es esta */
let myNull: null = null
let myUndefined: undefined = undefined

/* Podemos tener variables que inicialmente son null y luego se cargan datos */
let data: [number] | null;

Objetos anidados

type Direccion = {
  numero: number,
  calle: string,
  pais: string
}
type Reserva = {
  id: number,
  nombre?: string, //? indica que el atributo es opcional
  readonly value: number, // atributo de sólo lectura
  talla: Talla, // tipo enums
  direccion: Direccion
}

const reserva: Reserva = {
  id: 1,
  nombre: "p34",
  value: 800_000,
  talla: Talla.Chica,
  direccion: {
    numero: 687,
    calle: "av 5th",
    pais: "egypt"
  }
}

Funciones

type Sizes = "s" | "m" | "l" | "xl"

const createProduct = (
  title: string,
  createdAt: Date,
  stock: number,
  size?: Sizes // usamos el ? para los parametros que son opcionales
){
  return {
    title,
    createdAt,
    stock,
    size,
  }
}

const product1 = createProduct("t-shirt", new Date(), 6, "xl")
const product1 = createProduct("t-shirt", new Date(), 6)

/* Indicando que la funcion va a retornar un number */
const calcTotal = (proces: number[]): number => {
  let total = 0
  prices.forEach(item=> total += item)
  return total
}

/* Indicando que la funcion NO va a retornar valores */
const calcTotal = (proces: number[]): void => {
  let total = 0
  prices.forEach(item=> total += item)
  console.log(total)
}

/* Recibiendo un objeto tipado como parametro*/
/* Ejemplo 1 */
const login = (data:{ email: string, password: number })=>{
 // your code  
}

login({
  email: "user@email.com",
  password: "123",
})

/* Ejemplo 2 */
const addProduct = ( 
  data: {
    title: string,
    createdAt: Sate,
    stock: number,
    size?: Sizes,
}) =>{
  // Your code
}

/* Ejemplo 3 : Usando un typer personalizado */
type Product = {
    title: string,
    createdAt: Sate,
    stock: number,
    size?: Sizes,
}
const addProduct = (data: Product) =>{
  // Your code
}

Módulos: import y exports

// product.model.ts
export type Sizes = "s" | "m" | "l" | "xl"
export type Product = {
    title: string,
    createdAt: Sate,
    stock: number,
    size?: Sizes,
}
// product.service.ts
import { Product } from "./product.model" 

export const addProduct = (data: Product) =>{
  // Your code
}

export const calcStock = (data: Product): number =>{
  // Your code
}
// main.ts
import { addProduct, calcStock } from "./product.service" 

addProduct({
  title: "laptop",
  createdAt: new Date(),
  stock: 25,
  size?: "s",
})

nullish-coalescing

export const createProduct = (
  id: string | number.
  isNew: boolean,
  stock?: number,
) => {
  return {
    id,
    stock: stock || 10,
    isNew: isNew || true,
  }
}

// ¿Que ocurre cuando is new es false?
// va a retornar siempre true dado que
// false === false

// ¿Que ocurre cuando stock sea 0?
// va a retornar siempre 10 dado que
// 0 === false

// Para resolver este problema llega nullish-coalescing
const a = 0
const b = a ?? 10 // 0

const a = false
const b = a ?? true // false

// el operado ?? sólo retorna lo del lado derecho cuando
// cuando el valor es null or undefined

Valores predefinidos

export const createProduct = (
  id: string | number.
  isNew: boolean = true,
  stock?: number = 10,
) => {
  return {
    id,
    stock: stock
    isNew: isNew
  }
}

Sobrecarga de funciones

Sobrecargar o Overloading es la capacidad que nos permiten algunos lenguajes de programación orientados a objetos definir una función con declaraciones diferentes que se ejecutan en el mismo contexto.

Veamos un ejemplo:

Sin sobrecargar las funciones

// Si recibe un string retorna un array
// Si recibe un array de strings retorna un string
function parseStr(input: string | string[]): string | string[] {
  if (Array.isArray(input)) {
    return input.join(''); // string
  } else {
    return input.split(''); // string[]
  }
}

const rtaArray = parseStr('Nico');

// Al implementar la función que retornara un array, para poder usar
// métodos de array, TS nos obligara a validar que es un array
if (Array.isArray(rtaArray)) {
  rtaArray.reverse();
}

const rtaStr = parseStr(['N','i','c','o']);
// Al implementar la función que retornara un string, para poder usar
// métodos de string, TS nos obligara a validar que es un string
if (typeof rtaStr === 'string') {
  rtaStr.toLowerCase();
}

Sobrecargando las funciones

// Definimos cada implementación
export function parseStr(input: string): string[];
export function parseStr(input: string[]): string;

// Luego definimos la implementación
export function parseStr(input: string | string[]): string | string[] {
  if (Array.isArray(input)) {
    return input.join(''); // string
  } else if (typeof input === 'string'){
    return input.split(''); // string[]
  }
}

// De esta forma TS podra saber que tipo de dato tendremos en cada
// implementación sin tener que validar explicitamente
const rtaArray = parseStr('Nico');
rtaArray.reverse();

const rtaStr = parseStr(['N','i','c','o']);
rtaStr.toLowerCase();

Interfaces

Similares a los types pero con la capacidad de usar herencia

De esta forma con ayuda de Interface podemos generar modelos base con atributos y mediante el uso de un extend podemos heredar clases.

// base.model.ts
export interface BaseModel {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

// product.model.ts 
import { BaseModel } from './../base.model';

export interface Product extends BaseModel {
  title: string;
  stock: number;
}

// order.model.ts
import { BaseModel } from './../base.model';

export interface Order extends BaseModel {
  products: Product[];
  user: User;
}

Propiedades de solo lectura

usar readonly nos ayudara a prevenir que sobre escribamos atributos que no queremos que sean modificados, de esta forma TS nos avisara cuando intentemos modificar estos atributos marcados como readonly

// base.model.ts
export interface BaseModel {
  readonly id: string;
  createdAt: Date;
  updatedAt: Date;
}

Utility Types

TypeScript proporciona varios tipos de utilidades para facilitar las transformaciones de tipos comunes. Estas utilidades están disponibles a nivel global. (ver mas)

Omit, Partial

Omit: nos permite omitir los atributos que indicamos, su opuesto es Pick

Partial: nos permite crear objetos indicando solo alguno de sus atributos, su opuesto es Required

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

// Omit Omitir id, createdAt y updatedAt
export interface CreateProductDto extends Omit<Product, 'id' | 'createdAt' | 'updatedAt'>{
 categoryId: string; 
}


// Partial nos permite recibir un subconjunto del objeto original
// (pone todos los atributos como opcionales)
export interface UpdateProductDto extends Partial<CreateProductDto> {}

// Note que el update extiende desde el CreateProductDto para no 
// permitir la modificacion del id, createdAt y updatedAt

readonly

readonly: definirá todos los atributos del objeto como solo lectura

Un caso de uso frecuente es cuando queremos hacer una búsqueda, y creamos un objeto con los atributos que deben hacer match pero queremos asegurarnos de que estos no sean modificados durante la ejecución, usaremos readonly en conjunto con Partial (todos opcionales) dado que para una búsqueda podemos enviar 1 o mas atributos.

// product.model.ts 

// ...
export interface FindProductDto extends Readonly<Partial<Product>> {}
 

Acceder al tipado por indice

En determinados casos queremos definir un tipo basado en el tipado de otra propiedad, en estos casos podemos apoyarnos del tipado por indices.

//  El id del updateProduct debe ser el mismo que el del Product
export const updateProduct = (id: string) {
//
}

// En caso que cambie el tipo del id del Product, tocaria cambiarlo ac
// también, para evitarnos eso, podemos usar tipado por indice
export const updateProduct = (id: Product["id"]) {
//
}

ReadonlyArray

Evitar las mutaciones en los arrays.

const numbers: ReadonlyArray<number> = [1,2,2,2];

// no podremos usar estos métodos
numbers.push(9);
numbers.pop();
numbers.unshift(1);

// SI podremos usar estos métodos
numbers.filter(()=> {})
numbers.reduce(() => 0)
numbers.map(() => 0)


export interface FindProductDto extends Readonly<Partial<Omit<Product, 'tags'>>> {
  tags: ReadonlyArray<string>;
}
 // no nos permitira hacer un 
FindProductDto.tags?.pop()

// Pero si una reasignación
FindProductDto.tags = []

// Para evitar lo anterior, usaremos readOnly en el atributo
export interface FindProductDto extends Readonly<Partial<Omit<Product, 'tags'>>> {
  readonly tags: ReadonlyArray<string>;
}

Promesas: Indicando el tipo de retorno

function delay (time: number) {
  // Bastara con indicar el type entre <> para que TS sepa que type
  // va a retornar  esa promesa
  const promise = new Promise<string>((resolve) => {
    setTimeout(() => {
      resolve('string');
    }, time);
  });
  return promise;
  }

Genéricos

¿qué ocurre si quiere crear un componente que funcione con una variedad de tipos en lugar de solo con uno? Puede usar el tipo any, pero entonces no podrá aprovechar la ventaja del sistema de comprobación de tipos de TypeScript. ver mas

Los genéricos nos permite definir de forma dinámica el tipado, esto es similar a los argumentos que se pasan a una función, pero se utilizan para indicar el tipo de datos que se va a utilizar en cada llamado que hagamos

Cree funciones genéricas cuando el código sea una función o una clase que:

  • Funcione con varios tipos de datos.
  • Use ese tipo de datos en varios lugares.

Los genéricos pueden:

  • Proporcionar más flexibilidad a la hora de trabajar con tipos.
  • Permitir la reutilización de código.
  • Reducir la necesidad de usar el tipo any.
// Ejemplo 1
// La funcion getArray recibira un Tipo de forma dinámica
// llamado MyType y que sera utilizado dentro de la funcion
// multiples veces
function getArray<MyType>(items : MyType[]) : MyType[] {
  return new Array<MyType>().concat(items);
}

// asi la usamos con number
let numberArray = getArray<number>([5, 10, 15, 20]);

// asi la usamos con string
let stringArray = getArray<string>(['Cats', 'Dogs', 'Birds']);


// Ejemplo 2
// la función identity acepta dos parámetros, value y message, y devuelve 
// el parámetro value. Puede usar dos genéricos, T y U, para asignar 
// distintos tipos a cada parámetro y al tipo de valor devuelto.
function identity<T, U> (value: T, message: U) : T {
  console.log(message);
  return value
}

let returnNumber = identity<number, string>(100, 'Hello!');
let returnString = identity<string, string>('100', 'Hola!');
let returnBoolean = identity<boolean, string>(true, 'Bonjour!');

returnNumber = returnNumber * 100;   // OK
returnString = returnString * 100;   // Error: Type 'number' not assignable to type 'string'
returnBoolean = returnBoolean * 100; // Error: Type 'number' not assignable to type 'boolean'

Genericos en clases

import axios from 'axios';
import { UpdateProductDto } from '../dtos/product.dto';

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

// La clase puede recibir tipos dinamicamente
export class BaseHttpService<TypeClass> {

  constructor(
    protected url: string
  ) {}

  async getAll() {
    const { data } = await axios.get<TypeClass[]>(this.url);
    return data;
  }
  
  // Los métodos también pueden recibir tipos de forma dinamica
  async update<ID, DTO>(id: ID, changes: DTO) {
    const { data } = await axios.put(`${this.url}/${id}`, changes);
    return data;
  }
}

(async ()=> {
  const url1 = 'https://api.escuelajs.co/api/v1/products';
  // Aca al utilizar el generico, podemos implementar un service 
  // basado en Product
  const productService = new BaseHttpService<Product>(url1);

  const rta = await productService.getAll();
  console.log('products', rta.length);
  // Indicamos los tipos de forma dinamica al llamar al método
  productService.update<Product['id'], UpdateProductDto>(1, {
    title: 'asa',
  });

  const url2 = 'https://api.escuelajs.co/api/v1/categories';
  // Aca al utilizar el generico, podemos implementar un service 
  // basado en Category sin tener que modificar código
  const categoryService = new BaseHttpService<Category>(url2);

  const rta1 = await categoryService.getAll();
  console.log('categories', rta1.length);
})();