Typescript Con De Tuti

Introducción

¡Hola a todos! Aquí Gentleman al teclado, trayéndoles un análisis pormenorizado sobre TypeScript y cómo este lenguaje puede revolucionar la forma en que trabajamos en equipos de desarrollo. Este libro es una expansión de un vídeo que subí a YouTube, donde hablamos de las bases de TypeScript, las ventajas y cómo puede ayudar en un equipo. Vamos a sumergirnos no solo en el contenido del vídeo, sino que ampliaremos con ejemplos prácticos, código y reflexiones clave para que ustedes, mis queridos desarrolladores, puedan llevar su código a un nivel superior.

¿Por Qué TypeScript?

¿Qué es TypeScript?

TypeScript es un "superset" de JavaScript, lo que significa que tiene todo lo que JavaScript ofrece, pero añade más funcionalidades que son especialmente útiles en proyectos grandes o en equipos. Como mencioné en el vídeo, si JavaScript es bueno, TypeScript es JavaScript con esteroides.

// Ejemplo básico de TypeScript
let mensaje: string = '¡Hola, TypeScript!';
console.log(mensaje);

Ventajas de Usar TypeScript en Equipos

  1. Seguridad de Tipos: Reduce los errores comunes en JavaScript permitiendo especificar tipos de variables.
  2. Mantenibilidad: El código es más fácil de entender y mantener.
  3. Refactorización: Segura y fácil de realizar gracias al sistema de tipos.

Conceptos Fundamentales de TypeScript

Variables y Tipos

Uno de los pilares de TypeScript es su capacidad de tipado. Esto evita muchos errores comunes en JavaScript.

// Ejemplo de tipado en TypeScript
let esActivo: boolean = true;
let cantidad: number = 123;

Interfaces y Clases

TypeScript permite definir interfaces y clases, lo que facilita la implementación de patrones de diseño avanzados y la organización del código.

interface Usuario {
 nombre: string;
 edad: number;
}

class Empleado implements Usuario {
 constructor(public nombre: string, public edad: number) {}
}

TypeScript en la Práctica

Análisis de Casos Prácticos

Vamos a analizar el segmento del vídeo donde discutimos la mutabilidad de las variables y cómo TypeScript puede ayudar a controlarla.

// Ejemplo de inmutabilidad en TypeScript
let x: number = 10;
// x = "Cambio de tipo"; // Esto generará un error en TypeScript.

Creando Métodos Efectivos

Discutimos también cómo la falta de claridad en los tipos puede llevar a errores en métodos que parecen simples.

function suma(a: number, b: number): number {
 return a + b;
}

Mejores Prácticas y Patrones

Trabajando con Equipos

  • Claridad: Usa tipos siempre.
  • Documentación: Aprovecha las características de TypeScript para documentar el código.
  • Revisión de Código: Fomenta las revisiones de código que se centren en la mejora del tipado.

Herramientas y Extensiones

Hablaremos de herramientas que pueden integrarse con TypeScript para mejorar aún más el flujo de trabajo, como linters, formateadores de código, y más.

¿Qué es Transpilar?

Transpilar, en el mundo de la programación, es el proceso de convertir código escrito en un lenguaje (o versión de un lenguaje) a otro lenguaje (o versión de ese lenguaje). En nuestro caso, a menudo hablamos de convertir TypeScript a JavaScript. Básicamente, transpilar es como traducir.

¿Por Qué Necesitamos Transpilar?

TypeScript es increíble porque nos da superpoderes: tipos, interfaces, y un montón de ayudas para evitar errores. Pero los navegadores y Node.js no entienden TypeScript, ellos solo hablan JavaScript. Entonces, necesitamos un traductor, y ese traductor es el compilador de TypeScript (tsc).

Ejemplo Práctico

Vamos a ver esto en acción con un ejemplo sencillo. Imaginemos que tenemos un archivo TypeScript script.ts con el siguiente código:

// script.ts
let mensaje: string = 'Hola, mundo';
console.log(mensaje);

Este archivo contiene una variable mensaje de tipo string y un console.log para mostrarla. Ahora, para que nuestro navegador entienda este código, necesitamos transpilarlo a JavaScript. Esto lo hacemos con el comando tsc (TypeScript Compiler):

tsc script.ts

Después de ejecutar este comando, obtenemos un archivo script.js con el siguiente contenido:

// script.js
var mensaje = 'Hola, mundo';
console.log(mensaje);

Como podés ver, el compilador de TypeScript ha convertido (o "transpilado") el código TypeScript a JavaScript.

Un Poco Más de Magia: Configuración del Compilador

Podemos hacer mucho más con la configuración de nuestro compilador TypeScript. Por ejemplo, podemos definir cómo queremos que se comporte el proceso de transpilar mediante un archivo tsconfig.json. Aquí te dejo un ejemplo básico:

// tsconfig.json
{
 "compilerOptions": {
  "target": "es6", // Indica a qué versión de JavaScript queremos transpilar
  "outDir": "./dist", // Carpeta donde se guardarán los archivos transpilados
  "strict": true // Activa todas las comprobaciones estrictas
 },
 "include": ["src/**/*.ts"], // Archivos a incluir en la transpilation
 "exclude": ["node_modules"] // Archivos o carpetas a excluir
}

Haciendo Magia con tsc --watch

Si querés llevar tu flujo de trabajo al siguiente nivel, podés usar el comando tsc --watch, que va a estar atento a cualquier cambio en tus archivos TypeScript y automáticamente los va a transpilar a JavaScript. Es como tener un asistente personal que siempre está atento para ayudarte.

tsc --watch

En Resumen

Transpilar es un proceso esencial que nos permite escribir en lenguajes modernos y con mejores características, y luego convertir ese código a un lenguaje que los navegadores y Node.js pueden entender. Es como tener un traductor que convierte nuestras palabras en algo que todo el mundo puede entender.

Así que la próxima vez que escuches "transpilar", ya sabés que no es más que un proceso de traducción, asegurando que nuestras genialidades en TypeScript lleguen intactas y claras a cualquier entorno JavaScript.

Entendiendo el Tipado en JavaScript y TypeScript

JavaScript: Un Lenguaje con Tipado Dinámico

Aunque a simple vista no lo parezca, JavaScript sí cuenta con un sistema de tipos, pero es dinámico. Esto significa que el tipo de una variable puede cambiar a lo largo de la ejecución del programa, lo que introduce flexibilidad pero también cierta propensión a errores difíciles de rastrear. Aquí es donde JavaScript muestra tanto su flexibilidad como sus limitaciones, ya que esta característica puede llevar a confusiones en proyectos grandes o cuando se trabaja en equipo.

El motor V8 de JavaScript, que es el que utilizan la mayoría de los navegadores modernos como Chrome y Node.js, maneja el tipado dinámico de una manera muy particular. Cuando se define una variable o un método, V8 asigna un tipo inicial basado en el valor asignado. Esta información se guarda en un buffer de memoria.

// Ejemplo de tipado dinámico en JavaScript
let valor = 'Hola'; // Inicialmente es una cadena
valor = 100; // Ahora es un número

En este ejemplo, valor cambia de un string a un número. ¡Es como si tu perro de repente decidiera ser gato! Esto, aunque útil a veces, puede traer problemas.

Cuando el tipo de una variable cambia, V8 debe reestructurar la forma en que almacena esta variable en memoria. Esto implica recalcular y reasignar la memoria para la nueva forma del dato, lo cual puede ser costoso en términos de rendimiento.

let a = 1; // 'a' es un número
a = 'uno'; // 'a' ahora es un string

El motor V8 detecta el cambio de tipo y ajusta la memoria y las referencias internas para adaptarse al nuevo tipo. Este proceso puede ralentizar la ejecución si ocurre con frecuencia.

TypeScript: Estabilidad y Seguridad a Través del Tipado Estático

En contraste, TypeScript introduce un sistema de tipado estático, que obliga a definir el tipo de dato de las variables y funciones desde el comienzo. Esto ayuda a evitar muchos errores comunes en JavaScript al hacer que el código sea más predecible y más fácil de debuggear. Al utilizar TypeScript, se puede tener un control mucho más estricto sobre cómo se manejan los datos en las aplicaciones, lo que se traduce en un código más robusto y seguro.

let valor: number = 100;
// valor = "Hola"; // Esto causará un error en TypeScript

¡Y listo! Ahora valor no puede cambiar de tipo y te aseguras que siempre será un número. Como tener un perro que siempre será perro.

TypeScript: El Superhéroe del Desarrollo

Primero, imaginemos que TypeScript es un superhéroe. Su misión: salvarnos de los errores y las pesadillas del código JavaScript. Pero, como todo buen superhéroe, tiene sus límites y áreas de operación. En este caso, TypeScript solo usa sus superpoderes durante el desarrollo.

¿Qué Hace TypeScript?

TypeScript, como dijimos anteriormente, es JavaScript con esteroides. Te permite agregar tipos a tus variables y funciones, lo que ayuda a evitar errores comunes. Pero aquí viene el truco: cuando tu aplicación se ejecuta en el navegador o en Node.js, todo ese código de TypeScript se ha transformado (o transpilado) en JavaScript. Es como si nuestro superhéroe se quitara el traje y se pusiera un uniforme común y corriente.

Linters: Los Compañeros de Batalla

Ahora, hablemos de los linters. Son como los compañeros de equipo de nuestro superhéroe. Los linters, como ESLint, trabajan codo a codo con TypeScript para mantener tu código limpio y libre de errores. Mientras que TypeScript se enfoca en los tipos y la estructura del código, los linters se ocupan de las reglas de estilo y buenas prácticas.

Ejemplo Práctico

Vamos a ver un ejemplo para que esto quede más claro. Supongamos que estamos trabajando en una aplicación súper cool:

// TypeScript
function saludar(nombre: string): string {
 return `Hola, ${nombre}`;
}

const saludo = saludar('Gentleman');
console.log(saludo);

Acá tenemos una función saludar que toma un nombre de tipo string y devuelve un saludo. TypeScript nos asegura que siempre pasemos un string a esta función. Pero, cuando llega el momento de la verdad, esto es lo que se ejecuta en tu navegador:

// JavaScript transpilado
function saludar(nombre) {
 return 'Hola, ' + nombre;
}

var saludo = saludar('Gentleman');
console.log(saludo);

¡Voilá! Todo el código TypeScript se ha transformado en JavaScript.

Integración con ESLint

Ahora, sumemos a nuestro linter al equipo. Imagina que tenemos una regla que dice que siempre debemos usar comillas simples. Así se ve el archivo .eslintrc.json:

{
 "rules": {
  "quotes": ["error", "single"]
 }
}

Si alguien se pone rebelde y usa comillas dobles en lugar de simples, ESLint nos va a tirar de las orejas y nos va a recordar seguir las reglas:

// Código incorrecto según ESLint
function saludar(nombre: string): string {
 return 'Hola, ' + nombre; // ESLint nos va a avisar de este error
}

Con ESLint integrado, nuestro código va a mantenerse limpio y consistente, haciendo equipo con TypeScript para asegurarnos de que todo esté en orden.

En Resumen

TypeScript es nuestro superhéroe durante el desarrollo, ayudándonos a escribir código más seguro y predecible. Pero una vez que todo está listo para la acción, se transforma en JavaScript. Y con los linters como compañeros de batalla, mantenemos nuestro código en la línea.

Así que, la próxima vez que alguien te diga que TypeScript "solo sirve durante el desarrollo", sabrás que, aunque es cierto, es precisamente ahí donde marca la diferencia.

Ejemplo Práctico en TypeScript: El Uso de any y la Importancia del Tipado

Vamos a ver por qué, aunque TypeScript nos da herramientas poderosas, usarlas incorrectamente puede llevarnos a caer en los mismos problemas que podríamos tener en JavaScript. ¡Prepárense para un pequeño desafío mental!

Paso 1: Declarar una variable con un tipo específico

Vamos a comenzar con algo simple. En TypeScript, podemos especificar el tipo de una variable para asegurarnos de que siempre contenga el tipo correcto de valor. Miren este ejemplo:

let numero: number = 5;

Ahora, si intentamos asignar un valor de un tipo diferente, como un string, TypeScript nos mostrará un error. Esto es genial porque previene errores en tiempo de compilación. Vamos a ver:

numero = 'esto debería fallar'; // Error: Type 'string' is not assignable to type 'number'.

Paso 2: Uso del tipo any

El tipo any en TypeScript nos permite asignar cualquier tipo de valor a una variable, similar a lo que ocurre por defecto en JavaScript. Veamos:

let variableFlexible: any = 5;
variableFlexible = 'puedo ser un string también';
variableFlexible = true; // ¡Y ahora un booleano!

Con any, no hay errores de compilación, independientemente del tipo de dato que asignemos. Esto nos da mucha flexibilidad, pero también elimina las garantías de seguridad de tipo que TypeScript ofrece.

Pregunta provocativa

Ahora, aquí viene la pregunta clave: Si estamos usando any, ¿te parece que está bien? Si piensan que no... entonces no te gusta Javascript ! ya que sería lo mismo que usar 'any' en todas nuestras variables.

¡Exacto! La mayoría dirá que no es una buena práctica usar any, ya que perdemos todos los beneficios del tipado estático que TypeScript ofrece. Es como si volviéramos a JavaScript, donde podemos cometer fácilmente errores de tipo.

Usar any nos lleva de vuelta a la flexibilidad (y los peligros) de JavaScript. Si bien any puede ser útil en situaciones donde necesitamos una solución temporal o cuando trabajamos con bibliotecas de terceros para las cuales no tenemos tipos, deberíamos evitarlo en nuestro código principal. En TypeScript, la meta es aprovechar el sistema de tipos para escribir código más seguro y mantenible.

Tipos Primitivos en TypeScript

TypeScript enriquece el conjunto de tipos primitivos de JavaScript, proporcionando un control más robusto y opciones para la declaración de variables. Aquí están los principales tipos primitivos:

  1. Boolean: Valor verdadero o falso.

    let estaActivo: boolean = true;
    
  2. Number: Cualquier número, incluyendo decimales, hexadecimales, binarios y octales.

    let cantidad: number = 56;
    let hexadecimal: number = 0xf00d;
    let binario: number = 0b1010;
    let octal: number = 0o744;
    
  3. String: Cadenas de texto.

    let nombre: string = 'Gentleman';
    
  4. Array: Arreglos que pueden ser tipados.

    let listaDeNumeros: number[] = [1, 2, 3];
    let listaDeStrings: Array<string> = ['uno', 'dos', 'tres'];
    
  5. Tuple: Permiten expresar un arreglo con número fijo y tipos de elementos conocidos, pero no necesariamente del mismo tipo.

    let tupla: [string, number] = ['hola', 10];
    
  6. Enum: Un medio para dar nombres más amigables a conjuntos de valores numéricos.

    enum Color {
     Rojo,
     Verde,
     Azul,
    }
    let c: Color = Color.Verde;
    
  7. Any: Para valores que pueden cambiar de tipo en el tiempo, es una forma de decirle a TypeScript que maneje la variable como en JavaScript puro.

    let noEstoySeguro: any = 4;
    noEstoySeguro = 'quizás sea una cadena';
    noEstoySeguro = false; // ahora es un booleano
    
  8. Void: Ausencia de tener cualquier tipo, usado comúnmente como tipo de retorno en funciones que no retornan nada.

    function advertirUsuario(): void {
     console.log('Este es un aviso!');
    }
    
  9. Null y Undefined: Son subtipos de todos los otros tipos.

    let u: undefined = undefined;
    let n: null = null;
    

Inferencia de Tipos en TypeScript

TypeScript es inteligente cuando se trata de inferir los tipos de las variables basándose en la información disponible, como el valor inicial de las variables. Sin embargo, esta inferencia puede ser complicada.

let mensaje = 'Hola, mundo';
// `mensaje` es automáticamente inferido como `string`

Trampas de la Inferencia en TypeScript: Un Ejemplo Práctico

¡Hola, comunidad! Hoy vamos a profundizar en un tema fascinante y a veces complicado de TypeScript: las trampas de la inferencia de tipos, utilizando un ejemplo práctico que nos mostrará cómo TypeScript maneja la inferencia de tipos dentro de estructuras de control complejas.

Ejemplo Problemático

Consideremos el siguiente código TypeScript:

const arregloDeValores = [
 {
  numero: 1,
  label: 'label1',
 },
 {
  numero: 2,
 },
];

const metodo = (param: typeof arregloDeValores) => {
 const indexArray = [1, 2];

 indexArray.forEach(index => {
  if (param[index].label) {
   // param[index].label => string | undefined
   console.log(param[index].label); // param[index].label => string | undefined
  }
 });
};

En este ejemplo, tenemos un arreglo arregloDeValores que contiene objetos con las propiedades numero y label. Sin embargo, notarán que el segundo objeto en el arreglo no tiene definida la propiedad label. Esto hace que el tipo de label sea inferido como string | undefined.

Problema de Inferencia

Cuando pasamos arregloDeValores a la función metodo y usamos forEach para iterar sobre un arreglo de índices, hacemos una comprobación en cada iteración para ver si label está presente. Si bien dentro del bloque if, uno podría esperar que TypeScript entienda que label no es undefined debido a la comprobación, la realidad es que TypeScript aún considera que el tipo de param[index].label es string | undefined tanto dentro como fuera del if.

¿Por qué ocurre esto?

TypeScript no lleva un "estado" del tipo a través del flujo del código de la misma manera que lo haría en un contexto más simple. Aunque dentro del bloque if ya verificamos que label existe, TypeScript no tiene una "memoria" de esta comprobación para futuras referencias en el mismo bloque de código. Esto es especialmente cierto cuando estamos iterando o utilizando estructuras más complejas como forEach, for, etc., donde las comprobaciones de tipo no se "propagan" más allá del ámbito inmediato en el que se realizan.

Consejo para Manejar la Inferencia en Bloques Iterativos

Para manejar mejor estos casos y asegurar que el código sea tipo-seguro sin depender de la inferencia de TypeScript, podrías considerar las siguientes prácticas:

  1. Asignación a Variables Temporales: A veces, asignar a una variable temporal dentro del bloque puede ayudar.

    indexArray.forEach(index => {
     const label = param[index].label;
     if (label) {
      console.log(label); // TypeScript entiende que label es string aquí
     }
    });
    
  2. Refinamiento de Tipos con Tipos de Guardia: Utiliza tipos de guardia para refinar los tipos dentro de los bloques iterativos o condicionales.

    indexArray.forEach(index => {
     if (typeof param[index].label === 'string') {
      console.log(param[index].label); // Ahora es seguro que es un string
     }
    });
    

Clases, Interfaces, Enums y Const: ¿Cómo se Utilizan para Tipar en TypeScript?

Cada uno de estos elementos tiene su propia magia para ayudarnos a escribir código más limpio, escalable y seguro. Vamos a desglosar cada uno y ver cómo y cuándo utilizarlos. 🎩✨

1. Clases

Las clases en TypeScript no solo son una plantilla para crear objetos, sino que también pueden ser utilizadas como tipos.

class Automovil {
 constructor(public marca: string, public modelo: string) {}
}

let miAuto: Automovil = new Automovil('Toyota', 'Corolla');

En el ejemplo, Automovil no solo define una clase sino también un tipo. Cuando decimos let miAuto: Automovil, estamos utilizando la clase como un tipo, asegurando que miAuto cumpla con la estructura y comportamiento definidos en la clase Automovil.

2. Interfaces

Las interfaces son potentes en TypeScript por su habilidad de definir contratos de estructuras para clases, objetos y funciones sin generar JavaScript al compilar. Son ideales para definir formas de datos y se usan extensamente en la programación orientada a objetos y en la integración con bibliotecas.

interface Vehiculo {
 marca: string;
 arrancar(): void;
}

class Camion implements Vehiculo {
 constructor(public marca: string, public capacidadCarga: number) {}
 arrancar() {
  console.log('El camión está arrancando...');
 }
}

Las interfaces no solo permiten tipar objetos y clases, también pueden ser extendidas y combinadas, lo cual es excelente para mantener el código organizado y reutilizable.

3. Enums

¡Ok, gente! Hoy vamos a hablar de algo que está re piola en TypeScript: ¡los enums! Estos muchachitos te permiten definir un conjunto de constantes con nombre, haciendo tu código más legible y mantenible. Vamos a ver dos tipos: los enums numéricos y los enums de strings. ¡Aguantá que se viene lo bueno!

Enums Numéricos

Los enums numéricos son como una escalera: cada peldaño sube de a uno. Si no les decís nada, arrancan desde cero y van sumando de a uno. Mirá esto:

Ejemplo

enum Direction {
 Up, // 0
 Down, // 1
 Left, // 2
 Right, // 3
}

console.log(Direction.Up); // Salida: 0
console.log(Direction.Left); // Salida: 2

Pero, ¿qué pasa si querés empezar desde otro número? No hay drama, mirá:

enum Direction {
 Up = 1,
 Down, // 2
 Left, // 3
 Right, // 4
}

console.log(Direction.Up); // Salida: 1
console.log(Direction.Right); // Salida: 4

Enums de Strings

Ahora, si te pinta usar strings, TypeScript también te banca. Con los enums de strings, le ponés el valor que quieras a cada miembro. Son más verbosos, pero re claros.

Ejemplo

enum Direction {
 Up = 'UP',
 Down = 'DOWN',
 Left = 'LEFT',
 Right = 'RIGHT',
}

console.log(Direction.Up); // Salida: "UP"
console.log(Direction.Left); // Salida: "LEFT"

Uso Combinado

Ojo al piojo, podés combinar números y strings en un enum, pero ojo, no es muy recomendable porque puede quedar un choclo. Si te manda un código así, es probable que tengas ganas de tirarte de los pelos. Pero bueno, para los curiosos, acá va:

Ejemplo

enum Mixed {
 No = 0,
 Yes = 'YES',
}

console.log(Mixed.No); // Salida: 0
console.log(Mixed.Yes); // Salida: "YES"

4. Const Assertions

En TypeScript, const no solo define una constante a nivel de ejecución, sino que también puede ser utilizada para hacer afirmaciones de tipo (type assertions). Usando as const, podemos decirle a TypeScript que trate el tipo de manera más específica y literal.

let config = {
 nombre: 'Aplicación',
 version: 1,
} as const;

// config.nombre = "Otra App"; // Error, porque nombre es una constante.

Esto es especialmente útil para definir objetos con propiedades que nunca cambiarán sus valores una vez asignados.

Type vs Interface en TypeScript: Cuándo y Cómo Usarlos

Aunque ambos se pueden usar para definir tipos en TypeScript, tienen sus particularidades y casos de uso ideales. Vamos a desglosar las diferencias y entender cuándo es mejor usar cada uno. 🚀

¿Qué es interface?

Una interface en TypeScript se utiliza principalmente para describir la forma que deben tener los objetos. Es una manera de definir contratos dentro de tu código así como también contratos con código externo al tuyo.

interface Usuario {
 nombre: string;
 edad?: number;
}

function saludar(usuario: Usuario) {
 console.log(`Hola, ${usuario.nombre}`);
}

Las interfaces son ideales para la programación orientada a objetos en TypeScript, donde puedes usarlas para asegurar que ciertas clases implementen métodos y propiedades específicos.

Ventajas de usar interface

  1. Extensibilidad: Las interfaces son extendibles y pueden ser extendidas por otras interfaces. Usando extends, una interfaz puede heredar de otra, lo cual es excelente para mantener grandes bases de código bien organizadas.
  2. Fusionado de Declaraciones: TypeScript permite que las declaraciones de interface sean fusionadas automáticamente. Si defines la misma interfaz en diferentes lugares, TypeScript las combina en una sola interfaz.

¿Qué es type?

El alias de tipo type se puede utilizar para crear un tipo personalizado y puede ser asignado a cualquier tipo de dato, no sólo a objetos. Los type son más versátiles que las interfaces en ciertos aspectos.

type Punto = {
 x: number;
 y: number;
};

type D3Punto = Punto & { z: number };

Ventajas de usar type

  1. Tipos Unión e Intersección: Con type, puedes fácilmente utilizar tipos unión e intersección para combinar tipos existentes de formas complejas y útiles.

  2. Tipos Primitivos y Tuplas: Los type pueden ser utilizados para alias de tipos primitivos, uniones, intersecciones, y tuplas.

¿Cuándo usar interface o type?

  1. Usa interface cuando:

    • Necesitas definir un 'contrato' para clases o para la forma de objetos.
    • Quieres aprovechar las capacidades de extensión y fusión de interfaces.
    • Estás creando una librería de definiciones de tipo o una API que será usada en otros proyectos TypeScript.
  2. Usa type cuando:

    • Necesitas usar uniones o intersecciones.
    • Quieres usar tuplas y otros tipos que no pueden ser expresados con una interface.
    • Prefieres trabajar con tipos más flexibles y no necesitas extender o implementarlos desde clases.

El Concepto de Shape en TypeScript

Ahora vamos a hablar de un concepto fundamental en TypeScript que nos ayuda a manejar la estructura y el tipo de nuestros objetos: el shape. Este concepto es crucial para entender cómo TypeScript maneja la tipificación y cómo podemos sacarle el máximo provecho a nuestro código. Así que, ¡Con de Tuti! 🚀

¿Qué es el Shape?

El concepto de shape (o forma) en TypeScript se refiere a la estructura que debe tener un objeto para ser considerado de un cierto tipo. Básicamente, cuando definimos un tipo o una interfaz, estamos definiendo el shape que cualquier objeto de ese tipo debe seguir.

interface Usuario {
 nombre: string;
 edad: number;
}

let usuario: Usuario = {
 nombre: 'Juan',
 edad: 25,
};

En este ejemplo, Usuario define el shape que el objeto usuario debe tener: debe tener las propiedades nombre y edad de los tipos string y number, respectivamente.

Inferencia de Tipos y Shape

TypeScript es muy bueno inferiendo tipos basados en los valores que proporcionamos. Sin embargo, la inferencia de tipos también se basa en el shape de los objetos.

let otroUsuario = {
 nombre: 'Ana',
 edad: 30,
};

Aquí, TypeScript inferirá que otroUsuario tiene el shape { nombre: string; edad: number; } sin necesidad de que lo especifiquemos explícitamente.

Trampas de la Inferencia con Shape

A veces, confiar en la inferencia de tipos puede llevar a situaciones complicadas, especialmente cuando trabajamos con objetos complejos y arrays. Vamos a volver a ver un ejemplo de cómo esto puede ser problemático:

const arregloDeValores = [
 {
  numero: 1,
  label: 'label1',
 },
 {
  numero: 2,
 },
];

const metodo = (param: typeof arregloDeValores) => {
 const indexArray = [1, 2];

 indexArray.forEach(index => {
  if (param[index].label) {
   console.log(param[index].label);
  }
 });
};

En este caso ya antes visto, param[index].label sigue siendo string | undefined tanto fuera como dentro del if, por más que hemos comprobado su existencia. ¿Por qué pasa esto? Porque TypeScript no puede garantizar que el shape se mantendrá constante a lo largo de la iteración sin almacenar la comprobación en una variable.

Manejo Correcto del Shape

Para manejar correctamente estas situaciones, es mejor guardar las comprobaciones en una variable, lo cual le da a TypeScript una pista más clara sobre el shape:

const metodo = (param: typeof arregloDeValores) => {
 const indexArray = [1, 2];

 indexArray.forEach(index => {
  const item = param[index];
  if (item.label) {
   console.log(item.label);
  }
 });
};

Ahora, TypeScript entiende que item.label dentro del if es un string y no undefined.

Entendiendo union e intersección en TypeScript

Vamos a explorar dos operadores fundamentales en TypeScript que nos permiten manejar tipos de manera flexible y poderosa: | (unión) y & (intersección). Estos operadores son clave para definir tipos complejos y manejar diferentes escenarios en nuestros programas. ¡Vamos a sumergirnos en ellos!

Operador | (Unión)

El operador | en TypeScript se utiliza para combinar tipos de manera que un valor pueda ser de uno de esos tipos. Es decir, si tenemos TipoA | TipoB, estamos diciendo que una variable puede ser de tipo TipoA o de tipo TipoB.

type Resultado = 'éxito' | 'error';

let estado: Resultado;

estado = 'éxito'; // válido
estado = 'error'; // válido
estado = 'otro'; // inválido, TypeScript marcará un error

En este ejemplo, estado puede ser "éxito" o "error", pero no puede ser otro valor.

Combinación de Tipos con Propiedades Compartidas

Cuando utilizamos el operador | para combinar tipos que comparten algunas propiedades, estas propiedades se conservan en la unión solo si son comunes a todos los tipos incluidos. Veamos un ejemplo para entender mejor este concepto:

interface Perro {
 tipo: 'perro';
 ladra: boolean;
}

interface Gato {
 tipo: 'gato';
 maulla: boolean;
}

type Animal = Perro | Gato;

function procesarAnimal(animal: Animal) {
 // Solo podemos acceder a la propiedad 'tipo' común a ambos tipos
 console.log(animal.tipo);

 // Esto generaría un error, ya que 'ladra' o 'maulla' dependen del tipo específico
 // console.log(animal.ladra); // Error: 'ladra' no existe en el tipo 'Animal'.
 // console.log(animal.maulla); // Error: 'maulla' no existe en el tipo 'Animal'.
}

let miPerro: Perro = { tipo: 'perro', ladra: true };
let miGato: Gato = { tipo: 'gato', maulla: true };

procesarAnimal(miPerro); // Salida esperada: "perro"
procesarAnimal(miGato); // Salida esperada: "gato"

En este ejemplo, Animal es una unión de Perro y Gato. Aunque ambos tipos tienen la propiedad tipo, las propiedades específicas como ladra y maulla solo están disponibles cuando se trabaja con un tipo específico (Perro o Gato), no en el tipo Animal como un todo.

Operador & (Intersección)

Por otro lado, el operador & en TypeScript se utiliza para crear un tipo que tenga todas las propiedades de los tipos que estamos combinando. Es decir, TipoA & TipoB significa un tipo que tiene todas las propiedades de TipoA y todas las propiedades de TipoB.

interface Persona {
 nombre: string;
}

interface Empleado {
 salario: number;
}

type EmpleadoConNombre = Persona & Empleado;

let empleado: EmpleadoConNombre = {
 nombre: 'Juan',
 salario: 3000,
};

En este caso, EmpleadoConNombre es un tipo que tiene tanto nombre como salario, combinando las propiedades de Persona y Empleado.

Uso de | y & juntos

Podemos combinar | y & para crear tipos aún más complejos y específicos según nuestras necesidades:

type Opciones = { modo: 'modoA' | 'modoB' } & { tamaño: 'pequeño' | 'grande' };

let configuracion: Opciones = {
 modo: 'modoA',
 tamaño: 'pequeño',
};

En este ejemplo, configuracion debe tener tanto modo (que puede ser "modoA" o "modoB") como tamaño (que puede ser "pequeño" o "grande").

Diferencias Clave

  • | (Unión): Se usa para combinar tipos donde un valor puede ser de cualquiera de esos tipos.
  • & (Intersección): Se usa para combinar tipos donde un valor debe tener todas las propiedades de esos tipos.

Entendiendo typeof en TypeScript

Ahora vamos a ver el uso del operador typeof en TypeScript y cómo puede ayudarnos a manejar tipos complejos de manera más eficiente.

Concepto de typeof

En TypeScript, typeof es un operador que nos permite referirnos al tipo de una variable, propiedad o expresión en tiempo de compilación. Este operador devuelve el tipo estático de la expresión a la que se aplica. Es muy útil cuando necesitamos referirnos a un tipo existente en lugar de definirlo explícitamente.

let x = 10;
let y: typeof x; // y será del tipo 'number'

En el ejemplo anterior, typeof x se evalúa como el tipo de la variable x, que es number. Esto nos permite asignar el tipo de x a otra variable y sin tener que especificarlo manualmente.

Utilización para Tipos Complejos

Una de las mayores ventajas de typeof es su capacidad para manejar tipos complejos de manera más clara y concisa. Por ejemplo, cuando trabajamos con tipos que son el resultado de uniones o intersecciones complejas, podemos utilizar typeof para capturar esos tipos de manera eficiente.

interface Persona {
 nombre: string;
 edad: number;
}

type Empleado = {
 id: number;
 puesto: string;
} & typeof miPersona; // Captura el tipo de 'miPersona'

const miPersona = { nombre: 'Juan', edad: 30 };

let empleado: Empleado;

empleado = { id: 1, puesto: 'Desarrollador', nombre: 'Juan', edad: 30 };

En este ejemplo, typeof miPersona captura el tipo de miPersona, que es { nombre: string; edad: number; }. Luego, este tipo se combina (&) con las propiedades adicionales de Empleado. Esto nos permite definir Empleado de una manera que aprovecha directamente el tipo de miPersona sin tener que repetir su estructura.

Beneficios de typeof

  • Refleja Cambios Automáticamente: Si modificamos miPersona, el tipo de Empleado se ajustará automáticamente para reflejar esos cambios.

  • Evita Duplicación de Código: No necesitamos definir manualmente la estructura de Persona dos veces; typeof se encarga de mantener la consistencia.

  • Mantenimiento Simplificado: Cuando el tipo de miPersona cambia, los usos de typeof se actualizan automáticamente, reduciendo errores y tiempos de mantenimiento.

Explorando as const en TypeScript

Este operador puede ayudarnos a definir valores constantes inmutables, mejorando la seguridad y precisión de nuestro código. Vamos a ver cómo funciona y cómo podemos usarlo para sacarle el máximo provecho.

¿Qué es as const?

El operador as const le dice a TypeScript que trate el valor de una expresión como una constante literal. Esto significa que cada valor se considerará inmutable y su tipo se reducirá a su forma más específica posible. Esto es particularmente útil cuando queremos asegurarnos de que los valores no cambien en el futuro.

Ejemplo Básico

Comencemos con un ejemplo simple para ver cómo funciona as const:

let colores = ['rojo', 'verde', 'azul'] as const;

Sin as const, colores sería del tipo string[], lo que permite cualquier cadena en el array. Pero al usar as const, colores se convierte en un tipo literal específico: readonly ["rojo", "verde", "azul"]. Ahora, TypeScript sabe que colores contiene exactamente esos tres elementos y nada más.

Aplicación en Objetos

El uso de as const no se limita a arrays; también puede ser aplicado a objetos. Esto es especialmente útil cuando trabajamos con configuraciones o datos que no deberían cambiar.

const configuracion = {
 modo: 'producción',
 version: 1.2,
 opciones: {
  depuracion: false,
 },
} as const;

En este caso, el objeto configuracion tiene un tipo inmutable con los valores exactos que hemos definido. Esto significa que configuracion.modo es del tipo "producción", configuracion.version es del tipo 1.2, y configuracion.opciones.depuracion es del tipo false.

Beneficios de as const

  1. Inmutabilidad: Los valores no pueden ser cambiados, lo que previene errores accidentales.
  2. Tipos Literales: Los tipos se reducen a sus formas más específicas, mejorando la precisión del tipado.
  3. Seguridad de Tipo: Garantiza que los valores no se modifiquen en tiempo de ejecución, proporcionando mayor seguridad en el código.

Uso en Funciones

Veamos cómo as const puede mejorar la precisión en el contexto de funciones:

function obtenerConfiguracion() {
 return {
  modo: 'producción',
  version: 1.2,
  opciones: {
   depuracion: false,
  },
 } as const;
}

const config = obtenerConfiguracion();
// config.modo es "producción", no string
// config.version es 1.2, no number
// config.opciones.depuracion es false, no boolean

Aquí, la función obtenerConfiguracion devuelve un objeto cuyo tipo es inmutable gracias a as const. Esto asegura que los valores devueltos tengan los tipos más específicos posibles.

Return Type

El tipo utilitario ReturnType en TypeScript es una herramienta poderosa que nos permite inferir el tipo de retorno de una función. Esto es especialmente útil cuando trabajamos con funciones complejas y queremos asegurarnos de que el tipo de retorno se maneje de manera coherente en todo nuestro código.

Uso de ReturnType

ReturnType toma un tipo de función y devuelve el tipo del valor que esa función retorna. Veamos un ejemplo de cómo se puede usar esto en un contexto de la vida cotidiana.

Supongamos que estamos trabajando en una aplicación de gestión de tareas. Tenemos una función que crea una nueva tarea y queremos reutilizar el tipo de retorno de esta función en otras partes de nuestro código.

// Definición de la función que crea una nueva tarea
function crearTarea(titulo: string, descripcion: string) {
 return {
  id: Math.random().toString(36).substr(2, 9),
  titulo,
  descripcion,
  completada: false,
 };
}

// Usamos ReturnType para inferir el tipo de retorno de crearTarea
type Tarea = ReturnType<typeof crearTarea>;

// Ahora podemos usar el tipo Tarea en otras partes de nuestro código
const nuevaTarea: Tarea = crearTarea(
 'Comprar leche',
 'Ir al supermercado y comprar leche',
);

console.log(nuevaTarea);

// Implementación de una función que marca una tarea como completada
function completarTarea(tarea: Tarea): Tarea {
 return { ...tarea, completada: true };
}

const tareaCompletada = completarTarea(nuevaTarea);

console.log(tareaCompletada);

En este ejemplo:

  1. Definimos una función crearTarea que devuelve un objeto representando una nueva tarea.
  2. Usamos ReturnType para crear un tipo Tarea que representa el tipo del valor retornado por crearTarea.
  3. Utilizamos el tipo Tarea para declarar variables y tipos de retorno en otras funciones.

Esto nos asegura que si la estructura del objeto retornado por crearTarea cambia, TypeScript actualizará automáticamente el tipo Tarea, manteniendo nuestro código consistente y evitando errores.

Ventajas de Usar ReturnType

  • Consistencia: Asegura que el tipo de retorno de una función se maneje de manera coherente en todo el código.
  • Mantenimiento: Facilita el mantenimiento del código, ya que cualquier cambio en la función original se reflejará automáticamente en el tipo inferido.
  • Evita errores: Ayuda a evitar errores de tipo que pueden surgir al copiar manualmente tipos de retorno complejos.

ReturnType es una herramienta extremadamente útil en TypeScript para manejar tipos de retorno complejos y mantener nuestro código limpio y seguro. Utilízalo siempre que necesites reutilizar el tipo de retorno de una función en varias partes de tu código.

Aventura en TypeScript: Type Assertion y Casteo de Tipos

Vamos a explorar cómo utilizarlos correctamente, los cuidados que debemos tener, y la diferencia crucial entre unknown y any. ¡Vamos a darle!

¿Qué es Type Assertion?

Type Assertion es una forma de indicarle a TypeScript que trate una variable como si fuera de un tipo específico. Es como decirle al compilador: "Confía en mí, sé lo que estoy haciendo". Esto puede ser útil en situaciones donde estamos seguros del tipo de una variable, pero TypeScript no puede inferirlo correctamente.

Hay dos sintaxis principales para Type Assertion en TypeScript:

  1. Usando el operador as:

    let valor: any = 'Este es un string';
    let longitud: number = (valor as string).length;
    
  2. Usando el operador <type>:

    let valor: any = 'Este es un string';
    let longitud: number = (<string>valor).length;
    

Ambas sintaxis logran lo mismo, pero as es más comúnmente utilizada en código moderno de TypeScript, especialmente cuando se trabaja con JSX en React.

Cuidados con Type Assertion

Type Assertion es poderoso, pero también puede ser peligroso si se usa incorrectamente. Aquí hay algunas cosas a tener en cuenta:

  1. Confianza en el Tipo: Asegúrate de que la aserción sea válida. Si te equivocas, puedes introducir errores difíciles de detectar.

    let valor: any = 'Este es un string';
    let numero: number = valor as number; // ¡Error en tiempo de ejecución!
    
  2. Evitar aserciones innecesarias: No uses Type Assertion si TypeScript puede inferir el tipo correctamente.

    let valor = 'Este es un string'; // TypeScript infiere que 'valor' es un string
    let longitud: number = valor.length; // No se necesita Type Assertion
    

Casteo de Tipos en TypeScript

El casteo de tipos es similar a Type Assertion, pero a menudo se refiere a convertir un tipo a otro en tiempo de ejecución, algo más común en lenguajes como C# o Java. En TypeScript, el casteo generalmente se logra mediante funciones de conversión.

Ejemplo:

let valor: any = '123';
let numero: number = Number(valor); // Casteo de string a number

Aunque TypeScript es un superset de JavaScript, no agrega características de casteo explícito, sino que se basa en funciones de conversión de JavaScript.

unknown vs any: Conoce la Diferencia

any y unknown son dos tipos especiales en TypeScript que permiten trabajar con valores de cualquier tipo, pero tienen diferencias clave en su uso y seguridad.

  1. any:

    • Permite que cualquier valor sea asignado a una variable.
    • Desactiva todas las comprobaciones de tipo, lo que puede llevar a errores en tiempo de ejecución.
    • Debe usarse con moderación.
    let valor: any = 'Este es un string';
    valor = 42; // No hay error, pero puede causar problemas en tiempo de ejecución
    valor.metodoInexistente(); // No hay error en tiempo de compilación, pero fallará en tiempo de ejecución
    
  2. unknown:

    • También permite que cualquier valor sea asignado a una variable.
    • Obliga a realizar comprobaciones de tipo antes de acceder a las propiedades o métodos, haciendo el código más seguro.
    let valor: unknown = 'Este es un string';
    
    if (typeof valor === 'string') {
     console.log(valor.length); // Safe, TypeScript sabe que es un string
    }
    
    // valor.metodoInexistente(); // Error en tiempo de compilación
    

Ejemplo Combinado

Veamos un ejemplo que combine Type Assertion, any, y unknown:

function procesarValor(valor: unknown) {
 if (typeof valor === 'string') {
  let longitud = (valor as string).length;
  console.log(`La longitud del string es ${longitud}`);
 } else if (typeof valor === 'number') {
  let doble = (valor as number) * 2;
  console.log(`El doble del número es ${doble}`);
 } else {
  console.log('El valor no es ni un string ni un número');
 }
}

let valorAny: any = 'Texto';
procesarValor(valorAny);

valorAny = 100;
procesarValor(valorAny);

valorAny = true;
procesarValor(valorAny); // El valor no es ni un string ni un número

En este ejemplo, usamos unknown para recibir valores de cualquier tipo, luego verificamos su tipo antes de realizar operaciones específicas. También mostramos cómo any puede ser flexible, pero debe manejarse con cuidado para evitar errores.

Functional Overloading en TypeScript: ¡Pura Magia

Vamos a explorar cómo podemos definir múltiples firmas para una función y cómo utilizar tipos para que nuestras funciones cambien su output según el tipo de parámetro de entrada. ¡Vamos a darle!

¿Qué es Functional Overloading?

En TypeScript, functional overloading (sobrecarga de funciones) nos permite definir múltiples firmas para una función, de modo que pueda aceptar diferentes tipos de argumentos y comportarse de manera distinta según el tipo de entrada.

Esto es particularmente útil cuando tenemos una función que puede operar de diferentes maneras dependiendo de los parámetros que reciba.

Sintaxis Básica

La sintaxis básica para definir una sobrecarga de funciones en TypeScript incluye varias firmas de función seguidas de una implementación que cubre todos los casos.

function miFuncion(param: string): string;
function miFuncion(param: number): number;
function miFuncion(param: boolean): boolean;

// Implementación que cubre todas las sobrecargas
function miFuncion(
 param: string | number | boolean,
): string | number | boolean {
 if (typeof param === 'string') {
  return `String recibido: ${param}`;
 } else if (typeof param === 'number') {
  return param * 2;
 } else {
  return !param;
 }
}

// Uso de la función sobrecargada
console.log(miFuncion('Hola')); // String recibido: Hola
console.log(miFuncion(42)); // 84
console.log(miFuncion(true)); // false

En este ejemplo, miFuncion puede aceptar un string, un number o un boolean, y se comportará de manera diferente según el tipo del argumento.

Ejemplos Prácticos

  1. Función para manipular arrays y strings:

    function manipular(data: string): string[];
    function manipular(data: string[]): string;
    function manipular(data: string | string[]): string | string[] {
     if (typeof data === 'string') {
      return data.split('');
     } else {
      return data.join('');
     }
    }
    
    // Uso de la función sobrecargada
    console.log(manipular('Hola')); // ['H', 'o', 'l', 'a']
    console.log(manipular(['H', 'o', 'l', 'a'])); // "Hola"
    
  2. Función para manejar diferentes tipos de entradas y producir diferentes salidas:

    function calcular(input: number): number;
    function calcular(input: string): string;
    function calcular(input: number | string): number | string {
     if (typeof input === 'number') {
      return input * input;
     } else {
      return input.toUpperCase();
     }
    }
    
    // Uso de la función sobrecargada
    console.log(calcular(5)); // 25
    console.log(calcular('hola')); // "HOLA"
    

Consideraciones Importantes

  1. Implementación Unificada: La implementación de la función debe ser capaz de manejar todos los tipos de parámetros definidos en las firmas de sobrecarga.
  2. Retorno Compatible: El tipo de retorno debe ser compatible con todos los tipos definidos en las firmas de sobrecarga.
  3. Uso Apropiado de Type Guards: Es fundamental usar correctamente los type guards (typeof, instanceof) para asegurar que la implementación maneje adecuadamente cada tipo.

Caso con tipos complejos

Sobrecarga de Funciones con Tipos Complejos

Primero, definimos nuestras interfaces para los tipos complejos Gato y Perro, que extienden una interfaz base Animal.

interface Animal {
 tipo: string;
 sonido(): void;
}

interface Gato extends Animal {
 tipo: 'gato';
 raza: string;
}

interface Perro extends Animal {
 tipo: 'perro';
 color: string;
}

Luego, definimos las declaraciones de sobrecarga de la función procesarAnimal para especificar los tipos de entrada y los tipos de salida.

function procesarAnimal(animal: Gato): string;
function procesarAnimal(animal: Perro): number;

A continuación, implementamos la función procesarAnimal utilizando la sobrecarga de funciones. Dependiendo de si el parámetro es un Gato o un Perro, la función devolverá un string o un number, respectivamente.

function procesarAnimal(animal: Gato | Perro): string | number {
 if ('raza' in animal) {
  // El objeto es un gato
  console.log(`Es un gato de raza ${animal.raza}`);
  animal.sonido();
  return animal.raza;
 } else {
  // El objeto es un perro
  console.log(`Es un perro de color ${animal.color}`);
  animal.sonido();
  return animal.color.length;
 }
}

Implementación de los Tipos

Creamos instancias de Gato y Perro y utilizamos la función procesarAnimal para procesar estos objetos. Dependiendo del tipo de objeto, la función devolverá un string o un number.

const miGato: Gato = {
 tipo: 'gato',
 raza: 'Siamés',
 sonido: () => console.log('Miau'),
};
const miPerro: Perro = {
 tipo: 'perro',
 color: 'Negro',
 sonido: () => console.log('Guau'),
};

const resultadoGato = procesarAnimal(miGato); // Output: Es un gato de raza Siamés \n Miau
const resultadoPerro = procesarAnimal(miPerro); // Output: Es un perro de color Negro \n Guau

console.log(resultadoGato); // Output: Siamés
console.log(resultadoPerro); // Output: 5

En este ejemplo, procesarAnimal(miGato) devolverá la raza del gato como un string, mientras que procesarAnimal(miPerro) devolverá la longitud del color del perro como un number.

Ejemplo Adicional: Sobrecarga con Verificación de Propiedades

Ahora, veamos otro ejemplo utilizando sobrecarga de funciones y la verificación de propiedades con el operador in.

interface Vehiculo {
 tipo: string;
 velocidadMaxima(): void;
}

interface Coche extends Vehiculo {
 tipo: 'coche';
 marca: string;
}

interface Bicicleta extends Vehiculo {
 tipo: 'bicicleta';
 tipoDeFreno: string;
}

function describirVehiculo(vehiculo: Coche): string;
function describirVehiculo(vehiculo: Bicicleta): boolean;

function describirVehiculo(vehiculo: Coche | Bicicleta): string | boolean {
 if ('marca' in vehiculo) {
  // El objeto es un coche
  console.log(`Es un coche de marca ${vehiculo.marca}`);
  vehiculo.velocidadMaxima();
  return vehiculo.marca;
 } else {
  // El objeto es una bicicleta
  console.log(`Es una bicicleta con freno de tipo ${vehiculo.tipoDeFreno}`);
  vehiculo.velocidadMaxima();
  return vehiculo.tipoDeFreno.length > 5;
 }
}

const miCoche: Coche = {
 tipo: 'coche',
 marca: 'Toyota',
 velocidadMaxima: () => console.log('200 km/h'),
};
const miBicicleta: Bicicleta = {
 tipo: 'bicicleta',
 tipoDeFreno: 'disco',
 velocidadMaxima: () => console.log('30 km/h'),
};

const resultadoCoche = describirVehiculo(miCoche); // Output: Es un coche de marca Toyota \n 200 km/h
const resultadoBicicleta = describirVehiculo(miBicicleta); // Output: Es una bicicleta con freno de tipo disco \n 30 km/h

console.log(resultadoCoche); // Output: Toyota
console.log(resultadoBicicleta); // Output: false

En este ejemplo, describirVehiculo(miCoche) devolverá la marca del coche como un string, mientras que describirVehiculo(miBicicleta) devolverá un boolean indicando si la longitud del tipo de freno de la bicicleta es mayor a 5 caracteres.

Utilitarios de TypeScript: Helpers Esenciales

TypeScript ofrece una variedad de tipos utilitarios que facilitan la manipulación y gestión de tipos complejos. Estos helpers permiten transformar, filtrar y crear nuevos tipos basados en otros tipos existentes. A continuación, exploraremos algunos de los helpers más comunes y cómo se pueden utilizar en el desarrollo diario.

Partial

Partial<T> convierte todas las propiedades de un tipo T en opcionales. Es útil cuando queremos trabajar con versiones incompletas de un tipo.

interface Usuario {
 nombre: string;
 edad: number;
 email: string;
}

const usuarioParcial: Partial<Usuario> = {
 nombre: 'Juan',
};

Required

Required<T> convierte todas las propiedades de un tipo T en requeridas. Es el opuesto de Partial.

interface Configuracion {
 modoOscuro?: boolean;
 notificaciones?: boolean;
}

const configuracionCompleta: Required<Configuracion> = {
 modoOscuro: true,
 notificaciones: true,
};

Readonly

Readonly<T> convierte todas las propiedades de un tipo T en propiedades de solo lectura.

interface Libro {
 titulo: string;
 autor: string;
}

const libro: Readonly<Libro> = {
 titulo: '1984',
 autor: 'George Orwell',
};

// libro.titulo = 'Rebelión en la granja'; // Error: no se puede asignar a 'titulo' porque es una propiedad de solo lectura.

Record

Record<K, T> construye un tipo de objeto cuyas propiedades son claves del tipo K y valores del tipo T.

type Rol = 'admin' | 'usuario' | 'invitado';

const permisos: Record<Rol, string[]> = {
 admin: ['leer', 'escribir', 'borrar'],
 usuario: ['leer', 'escribir'],
 invitado: ['leer'],
};

Pick

Pick<T, K> crea un tipo seleccionando un subconjunto de las propiedades K de un tipo T.

interface Persona {
 nombre: string;
 edad: number;
 direccion: string;
}

const personaNombreEdad: Pick<Persona, 'nombre' | 'edad'> = {
 nombre: 'María',
 edad: 30,
};

Omit

Omit<T, K> crea un tipo omitiendo un subconjunto de las propiedades K de un tipo T.

interface Producto {
 id: number;
 nombre: string;
 precio: number;
}

const productoSinId: Omit<Producto, 'id'> = {
 nombre: 'Laptop',
 precio: 1500,
};

Exclude

Exclude<T, U> excluye de T los tipos que son asignables a U.

type NumerosOString = string | number | boolean;

type SoloNumerosOString = Exclude<NumerosOString, boolean>; // string | number

Extract

Extract<T, U> extrae de T los tipos que son asignables a U.

type Tipos = string | number | boolean;

type SoloBooleanos = Extract<Tipos, boolean>; // boolean

NonNullable

NonNullable<T> elimina null y undefined de un tipo T.

type PosiblementeNulo = string | number | null | undefined;

type SinNulos = NonNullable<PosiblementeNulo>; // string | number

ReturnType

ReturnType<T> obtiene el tipo de retorno de una función T.

function obtenerUsuario(id: number) {
 return { id, nombre: 'Juan' };
}

type Usuario = ReturnType<typeof obtenerUsuario>; // { id: number, nombre: string }

Ejemplo Completo

Vamos a ver un ejemplo práctico utilizando varios de estos helpers juntos:

interface Usuario {
 id: number;
 nombre: string;
 email?: string;
 direccion?: string;
}

// Convertir todas las propiedades a opcionales
type UsuarioParcial = Partial<Usuario>;

// Convertir todas las propiedades a requeridas
type UsuarioRequerido = Required<Usuario>;

// Crear un tipo de solo lectura
type UsuarioSoloLectura = Readonly<Usuario>;

// Seleccionar solo algunas propiedades
type UsuarioBasico = Pick<Usuario, 'id' | 'nombre'>;

// Omitir algunas propiedades
type UsuarioSinId = Omit<Usuario, 'id'>;

// Crear un registro de roles a permisos
type Rol = 'admin' | 'editor' | 'lector';
const permisos: Record<Rol, string[]> = {
 admin: ['crear', 'leer', 'actualizar', 'eliminar'],
 editor: ['crear', 'leer', 'actualizar'],
 lector: ['leer'],
};

// Excluir tipos
type ID = string | number | boolean;
type IDSinBooleanos = Exclude<ID, boolean>; // string | number

// Extraer tipos
type SoloBooleanos = Extract<ID, boolean>; // boolean

// Eliminar null y undefined
type PuedeSerNulo = string | null | undefined;
type NoNulo = NonNullable<PuedeSerNulo>; // string

Generics en TypeScript

Los genéricos en TypeScript son una poderosa herramienta que permite crear componentes reutilizables y altamente flexibles. Los genéricos proporcionan una forma de definir tipos de una manera que aún no está determinada, lo que permite que las funciones, clases y tipos trabajen con cualquier tipo especificado en el momento de la llamada o de la instanciación. A continuación, exploraremos los conceptos básicos y avanzados de los genéricos en TypeScript, incluyendo ejemplos prácticos.

Conceptos Básicos

Los genéricos se declaran utilizando la notación de ángulo <T>, donde T es un parámetro de tipo genérico. Este parámetro de tipo puede ser cualquier letra o palabra, aunque T es comúnmente usado.

Funciones Genéricas

Las funciones genéricas permiten trabajar con cualquier tipo de dato sin sacrificar la tipificación.

function identidad<T>(valor: T): T {
 return valor;
}

const numero = identidad<number>(42); // 42
const texto = identidad<string>('Hola Mundo'); // 'Hola Mundo'

Clases Genéricas

Las clases genéricas permiten crear estructuras de datos que pueden trabajar con cualquier tipo.

class Caja<T> {
 contenido: T;

 constructor(contenido: T) {
  this.contenido = contenido;
 }

 obtenerContenido(): T {
  return this.contenido;
 }
}

const cajaDeNumero = new Caja<number>(123);
console.log(cajaDeNumero.obtenerContenido()); // 123

const cajaDeTexto = new Caja<string>('Texto');
console.log(cajaDeTexto.obtenerContenido()); // 'Texto'

Interfaces Genéricas

Las interfaces genéricas permiten definir contratos que pueden adaptarse a diferentes tipos.

interface Par<K, V> {
 clave: K;
 valor: V;
}

const parNumeroTexto: Par<number, string> = { clave: 1, valor: 'Uno' };
const parTextoBooleano: Par<string, boolean> = { clave: 'activo', valor: true };

Uso Avanzado de Genéricos

Restricciones en Genéricos

Podemos restringir los tipos que un genérico puede aceptar usando extends.

interface ConNombre {
 nombre: string;
}

function saludar<T extends ConNombre>(obj: T): void {
 console.log(`Hola, ${obj.nombre}`);
}

saludar({ nombre: 'Juan' }); // Hola, Juan
// saludar({ apellido: 'Perez' }); // Error: el objeto no tiene la propiedad 'nombre'

Genéricos en Funciones de Orden Superior

Podemos utilizar genéricos en funciones que aceptan y retornan otras funciones.

function procesar<T>(elementos: T[], callback: (elemento: T) => void): void {
 elementos.forEach(callback);
}

procesar<number>([1, 2, 3], numero => console.log(numero * 2)); // 2, 4, 6

Ejemplo Completo con Tipos Complejos y in

A continuación, vamos a combinar lo aprendido sobre genéricos con una comprobación avanzada de tipos utilizando la palabra clave in.

interface Animal {
 tipo: string;
 sonido(): void;
}

interface Gato extends Animal {
 tipo: 'gato';
 raza: string;
}

interface Perro extends Animal {
 tipo: 'perro';
 color: string;
}

function procesarAnimal<T extends Animal>(animal: T): string {
 if ('raza' in animal) {
  return `Es un gato de raza ${animal.raza}`;
 } else if ('color' in animal) {
  return `Es un perro de color ${animal.color}`;
 } else {
  return `Es un animal de tipo desconocido`;
 }
}

const miGato: Gato = {
 tipo: 'gato',
 raza: 'Siamés',
 sonido: () => console.log('Miau'),
};
const miPerro: Perro = {
 tipo: 'perro',
 color: 'Negro',
 sonido: () => console.log('Guau'),
};

console.log(procesarAnimal(miGato)); // Output: Es un gato de raza Siamés
console.log(procesarAnimal(miPerro)); // Output: Es un perro de color Negro

En este ejemplo, procesarAnimal es una función genérica que puede procesar cualquier tipo de animal que extienda de Animal. Utilizamos la palabra clave in para verificar la existencia de una propiedad y así determinar el tipo exacto del objeto.

La Magia de los Enums

Primero, definimos dos enums. Los enums son esos amigos que siempre traen algo útil a la fiesta. Nos permiten agrupar constantes con nombre para que no tengamos que andar adivinando qué significa cada valor.

enum Numbers1 {
 'NUMBER1' = 'number1',
 'NUMBER2' = 'number2',
}

enum Numbers2 {
 'NUMBER3' = 'number3',
}

Combinando Superpoderes

Ahora, mezclamos estos dos enums en un solo objeto. Para esto, usamos el operador de propagación (...). Y ojo, porque as const es la clave acá para que TypeScript trate este objeto como una constante inamovible.

const myNumbers = { ...Numbers1, ...Numbers2 } as const;
const mixValues = Object.values(myNumbers);

Tipos Derivados de los Enums Combinados

¿Y ahora qué? Bueno, ahora usamos typeof y [number] para crear un tipo que represente los valores combinados de los enums. ¿Cómo es esto?

type MixNumbers = (typeof mixValues)[number];

Pero, ¿por qué [number]?

Buena pregunta, querido lector. Cuando hacemos Object.values(myNumbers), obtenemos un array de valores. Entonces, typeof mixValues nos da el tipo de este array, que es string[] en nuestro caso. Al usar [number], estamos diciendo "quiero el tipo de los elementos dentro de este array". Es como decirle a TypeScript: “Che, dame el tipo de lo que hay adentro, no del contenedor”.

Ahora, la razón más técnica y precisa: los enums en TypeScript generan una estructura interna que usa tanto las claves como los valores para crear una especie de bi-direccionalidad. Esto significa que en el objeto enum, cada valor tiene una clave numérica asociada automáticamente. Cuando usamos [number], estamos aprovechando esta característica para obtener el tipo de los valores que están siendo indexados numéricamente.

Creando un Tipo Basado en Nuestros Valores

Finalmente, creamos un tipo Enums que utiliza un índice mapeado para definir propiedades basadas en los valores de MixNumbers. Cada propiedad puede ser de cualquier tipo (any), porque a veces la vida es así de flexible.

type Enums = {
 [key in MixNumbers]: any;
};

El Ejemplo Completo

Vamos a ver el código completo en acción:

enum Numbers1 {
 'NUMBER1' = 'number1',
 'NUMBER2' = 'number2',
}

enum Numbers2 {
 'NUMBER3' = 'number3',
}

const myNumbers = { ...Numbers1, ...Numbers2 } as const;
const mixValues = Object.values(myNumbers);

type MixNumbers = (typeof mixValues)[number];

type Enums = {
 [key in MixNumbers]: any;
};

// Ejemplo de uso
const example: Enums = {
 number1: 'Este es el número 1',
 number2: 42,
 number3: { detalle: 'Número 3 como objeto' },
};

console.log(example);

Desglose del Código

  1. Definición de Enums: Numbers1 y Numbers2 son nuestros superhéroes iniciales, cada uno con sus propios poderes.
  2. Combinación de Enums: Mezclamos los poderes de nuestros héroes en un solo equipo utilizando ... y as const.
  3. Creación de Tipos Derivados: Utilizamos typeof y [number] para crear un tipo que representa los valores combinados.
  4. Definición del Tipo Enums: Usamos un índice mapeado para definir propiedades basadas en MixNumbers.