Clase 18 - TypeScript Avanzado
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 opcionalestype 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 nombreupdateUser(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 necesitamostype 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 sensibletype 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 numbertype StatusCodes = Record<string, number>;
const httpCodes: StatusCodes = { 'OK': 200, 'NOT_FOUND': 404, 'SERVER_ERROR': 500};
// Usar con tipos más específicostype 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óntype Colors = 'red' | 'green' | 'blue' | 'yellow';type PrimaryColors = Exclude<Colors, 'yellow'>; // 'red' | 'green' | 'blue'
// Extract<T, U> - Extrae tipos de una unióntype ExtractedColors = Extract<Colors, 'red' | 'blue'>; // 'red' | 'blue'
// NonNullable<T> - Excluye null y undefinedtype MaybeString = string | null | undefined;type DefinitelyString = NonNullable<MaybeString>; // string
// ReturnType<T> - Obtiene el tipo de retorno de una funciónfunction 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 simplefunction identity<T>(arg: T): T { return arg;}
// Uso con diferentes tiposconst stringResult = identity<string>("Hola mundo"); // stringconst numberResult = identity<number>(42); // numberconst booleanResult = identity(true); // TypeScript infiere el tipo boolean
// Función genérica más complejafunction getFirstElement<T>(array: T[]): T | undefined { return array.length > 0 ? array[0] : undefined;}
const firstNumber = getFirstElement([1, 2, 3]); // number | undefinedconst 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éricaconst 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 lengthinterface Lengthwise { length: number;}
function logLength<T extends Lengthwise>(arg: T): T { console.log(`Longitud: ${arg.length}`); return arg;}
logLength("Hola"); // ✅ string tiene lengthlogLength([1, 2, 3]); // ✅ array tiene length// logLength(123); // ❌ Error: number no tiene length
// Constraint con keyof - asegurar que la clave existe en el objetofunction 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"); // stringconst age = getProperty(person, "age"); // number// const invalid = getProperty(person, "height"); // ❌ Error: 'height' no existe🎛️ Mapped Types con Generics
// Crear tipos que transforman otros tipostype 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 avanzadatype 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 funcionesfunction 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.