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
- Seguridad de Tipos: Reduce los errores comunes en JavaScript permitiendo especificar tipos de variables.
- Mantenibilidad: El código es más fácil de entender y mantener.
- 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
any
y la Importancia del TipadoVamos 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:
-
Boolean: Valor verdadero o falso.
let estaActivo: boolean = true;
-
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;
-
String: Cadenas de texto.
let nombre: string = 'Gentleman';
-
Array: Arreglos que pueden ser tipados.
let listaDeNumeros: number[] = [1, 2, 3]; let listaDeStrings: Array<string> = ['uno', 'dos', 'tres'];
-
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];
-
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;
-
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
-
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!'); }
-
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:
-
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í } });
-
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
- 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. - 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
-
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. -
Tipos Primitivos y Tuplas: Los
type
pueden ser utilizados para alias de tipos primitivos, uniones, intersecciones, y tuplas.
¿Cuándo usar interface
o type
?
-
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.
-
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
typeof
en TypeScriptAhora 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 deEmpleado
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 detypeof
se actualizan automáticamente, reduciendo errores y tiempos de mantenimiento.
Explorando as const
en TypeScript
as const
en TypeScriptEste 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
- Inmutabilidad: Los valores no pueden ser cambiados, lo que previene errores accidentales.
- Tipos Literales: Los tipos se reducen a sus formas más específicas, mejorando la precisión del tipado.
- 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:
- Definimos una función
crearTarea
que devuelve un objeto representando una nueva tarea. - Usamos
ReturnType
para crear un tipoTarea
que representa el tipo del valor retornado porcrearTarea
. - 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:
-
Usando el operador
as
:let valor: any = 'Este es un string'; let longitud: number = (valor as string).length;
-
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:
-
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!
-
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
unknown
vs any
: Conoce la Diferenciaany
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.
-
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
-
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
-
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"
-
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
- 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.
- Retorno Compatible: El tipo de retorno debe ser compatible con todos los tipos definidos en las firmas de sobrecarga.
- 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
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
- Definición de Enums:
Numbers1
yNumbers2
son nuestros superhéroes iniciales, cada uno con sus propios poderes. - Combinación de Enums: Mezclamos los poderes de nuestros héroes en un solo
equipo utilizando
...
yas const
. - Creación de Tipos Derivados: Utilizamos
typeof
y[number]
para crear un tipo que representa los valores combinados. - Definición del Tipo Enums: Usamos un índice mapeado para definir
propiedades basadas en
MixNumbers
.