Saltar al contenido

Clase 18 - TypeScript Avanzado

#ts #generics #utilities #clase

TypeScript Avanzado: Utility Types, Types en Clases y Generic Types

1. 🛠️ Utility Types

Los Utility Types son tipos predefinidos en TypeScript que nos permiten transformar y manipular otros tipos de manera eficiente. Son herramientas poderosas que nos ayudan a crear código más flexible y reutilizable.

📋 Partial<T>

Convierte todas las propiedades de un tipo en opcionales.

interface User {
id: number;
name: string;
email: string;
age: number;
}
// Todas las propiedades se vuelven opcionales
type PartialUser = Partial<User>;
// Equivale a:
// {
// id?: number;
// name?: string;
// email?: string;
// age?: number;
// }
function updateUser(id: number, updates: Partial<User>) {
// Podemos actualizar solo algunas propiedades
console.log(`Actualizando usuario ${id}`, updates);
}
updateUser(1, { name: "Juan Carlos" }); // Solo actualiza el nombre
updateUser(2, { email: "nuevo@email.com", age: 30 }); // Actualiza email y edad

🔒 Required<T>

Convierte todas las propiedades opcionales de un tipo en requeridas.

interface UserConfig {
theme?: string;
language?: string;
notifications?: boolean;
}
type RequiredUserConfig = Required<UserConfig>;
// Equivale a:
// {
// theme: string;
// language: string;
// notifications: boolean;
// }
function validateConfig(config: Required<UserConfig>) {
// Todas las propiedades son obligatorias aquí
console.log(config.theme); // No hay riesgo de undefined
console.log(config.language);
console.log(config.notifications);
}

🎯 Pick<T, K>

Crea un nuevo tipo seleccionando solo propiedades específicas de un tipo existente.

interface Product {
id: number;
name: string;
description: string;
price: number;
category: string;
inStock: boolean;
}
// Solo seleccionamos las propiedades que necesitamos
type ProductSummary = Pick<Product, 'id' | 'name' | 'price'>;
// Equivale a:
// {
// id: number;
// name: string;
// price: number;
// }
function displayProductCard(product: ProductSummary) {
return `${product.name} - $${product.price}`;
}

🚫 Omit<T, K>

Crea un nuevo tipo excluyendo propiedades específicas de un tipo existente.

interface Employee {
id: number;
name: string;
email: string;
salary: number;
department: string;
}
// Excluimos información sensible
type PublicEmployee = Omit<Employee, 'salary'>;
// Equivale a:
// {
// id: number;
// name: string;
// email: string;
// department: string;
// }
function getPublicEmployeeInfo(employee: Employee): PublicEmployee {
const { salary, ...publicInfo } = employee;
return publicInfo;
}

📝 Record<K, T>

Crea un tipo con propiedades de tipo K y valores de tipo T.

// Crear un diccionario con claves string y valores number
type StatusCodes = Record<string, number>;
const httpCodes: StatusCodes = {
'OK': 200,
'NOT_FOUND': 404,
'SERVER_ERROR': 500
};
// Usar con tipos más específicos
type Theme = 'light' | 'dark' | 'auto';
type ThemeConfig = Record<Theme, { background: string; text: string }>;
const themeSettings: ThemeConfig = {
light: { background: '#ffffff', text: '#000000' },
dark: { background: '#000000', text: '#ffffff' },
auto: { background: 'system', text: 'system' }
};

🔗 Otros Utility Types útiles

// Exclude<T, U> - Excluye tipos de una unión
type Colors = 'red' | 'green' | 'blue' | 'yellow';
type PrimaryColors = Exclude<Colors, 'yellow'>; // 'red' | 'green' | 'blue'
// Extract<T, U> - Extrae tipos de una unión
type ExtractedColors = Extract<Colors, 'red' | 'blue'>; // 'red' | 'blue'
// NonNullable<T> - Excluye null y undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
// ReturnType<T> - Obtiene el tipo de retorno de una función
function getUser() {
return { id: 1, name: 'Juan' };
}
type UserReturnType = ReturnType<typeof getUser>; // { id: number; name: string; }

2. 🏛️ Types en Clases

TypeScript nos permite usar tipos de manera avanzada en clases, incluyendo propiedades, métodos, herencia y modificadores de acceso.

🎯 Definición básica de clases con tipos

class User {
// Propiedades con tipos explícitos
public id: number;
public name: string;
private email: string;
protected createdAt: Date;
readonly role: string;
constructor(id: number, name: string, email: string, role: string = 'user') {
this.id = id;
this.name = name;
this.email = email;
this.role = role;
this.createdAt = new Date();
}
// Método con tipos en parámetros y retorno
public updateEmail(newEmail: string): boolean {
if (this.isValidEmail(newEmail)) {
this.email = newEmail;
return true;
}
return false;
}
// Método privado
private isValidEmail(email: string): boolean {
return email.includes('@');
}
// Getter con tipo de retorno
public getEmail(): string {
return this.email;
}
}

🔧 Propiedades automáticas en constructor

class Product {
// Declaración automática de propiedades en el constructor
constructor(
public id: number,
public name: string,
private price: number,
protected category: string,
readonly createdAt: Date = new Date()
) {}
public getPrice(): number {
return this.price;
}
public applyDiscount(percentage: number): void {
if (percentage > 0 && percentage <= 100) {
this.price = this.price * (1 - percentage / 100);
}
}
}

🏗️ Clases abstractas con tipos

abstract class Animal {
protected name: string;
protected age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// Método concreto
public getInfo(): string {
return `${this.name} tiene ${this.age} años`;
}
// Método abstracto que debe ser implementado por las subclases
abstract makeSound(): string;
abstract move(): void;
}
class Dog extends Animal {
private breed: string;
constructor(name: string, age: number, breed: string) {
super(name, age);
this.breed = breed;
}
public makeSound(): string {
return 'Guau guau!';
}
public move(): void {
console.log(`${this.name} está corriendo`);
}
public getBreed(): string {
return this.breed;
}
}

🎭 Interfaces con clases

interface Flyable {
altitude: number;
fly(): void;
land(): void;
}
interface Swimmable {
depth: number;
swim(): void;
dive(depth: number): void;
}
class Duck extends Animal implements Flyable, Swimmable {
public altitude: number = 0;
public depth: number = 0;
constructor(name: string, age: number) {
super(name, age);
}
public makeSound(): string {
return 'Cuac cuac!';
}
public move(): void {
console.log(`${this.name} está caminando`);
}
public fly(): void {
this.altitude = 10;
console.log(`${this.name} está volando a ${this.altitude} metros`);
}
public land(): void {
this.altitude = 0;
console.log(`${this.name} ha aterrizado`);
}
public swim(): void {
console.log(`${this.name} está nadando`);
}
public dive(depth: number): void {
this.depth = depth;
console.log(`${this.name} se ha sumergido a ${depth} metros`);
}
}

🔐 Modificadores de acceso avanzados

class BankAccount {
// Propiedad estática
private static readonly BANK_NAME: string = 'Mi Banco';
private static accountCount: number = 0;
// Propiedades de instancia
private readonly accountNumber: string;
private balance: number;
protected overdraftLimit: number;
constructor(initialBalance: number, overdraftLimit: number = 0) {
this.accountNumber = this.generateAccountNumber();
this.balance = initialBalance;
this.overdraftLimit = overdraftLimit;
BankAccount.accountCount++;
}
// Método estático
public static getTotalAccounts(): number {
return BankAccount.accountCount;
}
public static getBankName(): string {
return BankAccount.BANK_NAME;
}
private generateAccountNumber(): string {
return `ACC-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
}
}
public withdraw(amount: number): boolean {
if (amount > 0 && (this.balance - amount) >= -this.overdraftLimit) {
this.balance -= amount;
return true;
}
return false;
}
public getBalance(): number {
return this.balance;
}
public getAccountNumber(): string {
return this.accountNumber;
}
}

3. 🧬 Generic Types

Los Generic Types permiten crear componentes reutilizables que funcionan con diferentes tipos, manteniendo la seguridad de tipos. Son fundamentales para escribir código flexible y type-safe.

🌟 Conceptos básicos de Generics

// Función genérica simple
function identity<T>(arg: T): T {
return arg;
}
// Uso con diferentes tipos
const stringResult = identity<string>("Hola mundo"); // string
const numberResult = identity<number>(42); // number
const booleanResult = identity(true); // TypeScript infiere el tipo boolean
// Función genérica más compleja
function getFirstElement<T>(array: T[]): T | undefined {
return array.length > 0 ? array[0] : undefined;
}
const firstNumber = getFirstElement([1, 2, 3]); // number | undefined
const firstName = getFirstElement(['Ana', 'Juan']); // string | undefined

📦 Interfaces genéricas

interface Container<T> {
value: T;
getValue(): T;
setValue(value: T): void;
}
class Box<T> implements Container<T> {
private _value: T;
constructor(value: T) {
this._value = value;
}
getValue(): T {
return this._value;
}
setValue(value: T): void {
this._value = value;
}
}
// Uso de la clase genérica
const stringBox = new Box<string>("Contenido");
const numberBox = new Box<number>(123);
const objectBox = new Box<{ name: string }>({ name: "Juan" });

🔗 Constraints (Restricciones) en Generics

// Constraint básico - T debe tener una propiedad length
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(`Longitud: ${arg.length}`);
return arg;
}
logLength("Hola"); // ✅ string tiene length
logLength([1, 2, 3]); // ✅ array tiene length
// logLength(123); // ❌ Error: number no tiene length
// Constraint con keyof - asegurar que la clave existe en el objeto
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Ana", age: 30, city: "Madrid" };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
// const invalid = getProperty(person, "height"); // ❌ Error: 'height' no existe

🎛️ Mapped Types con Generics

// Crear tipos que transforman otros tipos
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Stringify<T> = {
[K in keyof T]: string;
};
type Prettify<T> = {
[K in keyof T]: T[K];
}
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
type OptionalUser = Optional<User>;
// {
// id?: number;
// name?: string;
// email?: string;
// isActive?: boolean;
// }
type StringifiedUser = Stringify<User>;
// {
// id: string;
// name: string;
// email: string;
// isActive: string;
// }

🔄 Conditional Types con Generics

// Tipos condicionales para lógica avanzada
type ApiResponse<T> = T extends string
? { message: T; status: 'success' }
: { data: T; status: 'success' };
type StringResponse = ApiResponse<string>;
// { message: string; status: 'success' }
type UserResponse = ApiResponse<User>;
// { data: User; status: 'success' }
// Ejemplo práctico con funciones
function processApiResponse<T>(data: T): ApiResponse<T> {
if (typeof data === 'string') {
return { message: data, status: 'success' } as ApiResponse<T>;
}
return { data, status: 'success' } as ApiResponse<T>;
}

4. 🎯 Ejercicios prácticos

Ejercicio 1: Utility Types

Crea un sistema de gestión de productos que use Partial, Pick y Omit para diferentes operaciones ABM.

Ejercicio 2: Clases con Types

Implementa un sistema de usuarios con roles, usando herencia, interfaces y modificadores de acceso apropiados.

Ejercicio 3: Generic Types

Construye un sistema de cache genérico que pueda almacenar cualquier tipo de dato con TTL (time to live). Utiliza una clase con métodos set, get y cleanup.