Objetos y clases
En este módulo, profundizaremos en el manejo de objetos y clases en TypeScript. Aprenderás cómo definir y trabajar con objetos, utilizar interfaces para tipar estructuras de datos, y crear clases con propiedades, métodos y constructores. Además, exploraremos los modificadores de acceso, la herencia, el polimorfismo, y el uso de interfaces y clases abstractas para diseñar sistemas más robustos y flexibles.
1. Objetos y sus propiedades #
En TypeScript, los objetos son colecciones de propiedades, donde cada propiedad tiene un nombre y un tipo. Al definir objetos, puedes especificar claramente qué propiedades deben existir y qué tipos de datos deben contener.
Definición básica de un objeto:
1let persona: { 2 nombre: string; 3 edad: number; 4 esEstudiante: boolean; 5} = { 6 nombre: "Ana", 7 edad: 28, 8 esEstudiante: false 9}; 10 11console.log(persona);Salida de consola { nombre: 'Ana', edad: 28, esEstudiante: false }
Accediendo a las propiedades:
1let persona: { 2 nombre: string; 3 edad: number; 4 esEstudiante: boolean; 5} = { 6 nombre: "Ana", 7 edad: 28, 8 esEstudiante: false 9}; 10 11console.log(`Nombre: ${persona.nombre}`); // "Nombre: Ana" 12console.log(`Edad: ${persona.edad}`); // "Edad: 28"Salida de consola Nombre: Ana Edad: 28
Actualizando propiedades:
1let persona: { 2 nombre: string; 3 edad: number; 4 esEstudiante: boolean; 5} = { 6 nombre: "Ana", 7 edad: 28, 8 esEstudiante: false 9}; 10 11persona.edad = 29; 12console.log(`Edad actualizada: ${persona.edad}`); // "Edad actualizada: 29"Salida de consola Edad actualizada: 29
Tipos de propiedades opcionales:
Puedes definir propiedades opcionales utilizando el signo ?.
1let coche: { 2 marca: string; 3 modelo: string; 4 año?: number; // Propiedad opcional 5} = { 6 marca: "Toyota", 7 modelo: "Corolla" 8}; 9 10console.log(coche); // { marca: 'Toyota', modelo: 'Corolla' }Salida de consola { marca: 'Toyota', modelo: 'Corolla' }
Interfaces para objetos #
Las interfaces en TypeScript son estructuras que definen la forma de un objeto. Son extremadamente útiles para asegurar que los objetos cumplan con ciertos contratos, mejorando la mantenibilidad y la claridad del código.
Definiendo una interfaz:
1interface Persona { 2 nombre: string; 3 edad: number; 4 esEstudiante: boolean; 5} 6 7let alumno: Persona = { 8 nombre: "Luis", 9 edad: 22, 10 esEstudiante: true 11}; 12 13console.log(alumno);Salida de consola { nombre: 'Luis', edad: 22, esEstudiante: true }
Interfaces con propiedades opcionales:
1interface Coche { 2 marca: string; 3 modelo: string; 4 año?: number; // Propiedad opcional 5} 6 7let miCoche: Coche = { 8 marca: "Honda", 9 modelo: "Civic" 10}; 11 12console.log(miCoche); // { marca: 'Honda', modelo: 'Civic' }Salida de consola { marca: 'Honda', modelo: 'Civic' }
Interfaces con propiedades de solo lectura:
1interface Libro { 2 readonly titulo: string; 3 autor: string; 4} 5 6let miLibro: Libro = { 7 titulo: "1984", 8 autor: "George Orwell" 9}; 10 11miLibro.titulo = "Animal Farm"; // <--- Error.Salida de consola error TS2540: Cannot assign to 'titulo' because it is a read-only property.
Interfaces con métodos:
1interface Calculadora { 2 sumar(a: number, b: number): number; 3 restar(a: number, b: number): number; 4} 5 6let calc: Calculadora = { 7 sumar: (a, b) => a + b, 8 restar: (a, b) => a - b 9}; 10 11console.log(calc.sumar(5, 3)); // 8 12console.log(calc.restar(5, 3)); // 2Salida de consola 8 2
Clases: Propiedades, métodos y constructores #
Las clases en TypeScript permiten crear objetos con una estructura definida, incluyendo propiedades y métodos. Además, los constructores facilitan la inicialización de las propiedades de una clase al momento de crear una instancia.
Definición básica de una clase:
1class Persona { 2 nombre: string; 3 edad: number; 4 esEstudiante: boolean; 5 6 constructor(nombre: string, edad: number, esEstudiante: boolean) { 7 this.nombre = nombre; 8 this.edad = edad; 9 this.esEstudiante = esEstudiante; 10 } 11 12 saludar(): void { 13 console.log(`Hola, me llamo ${this.nombre} y tengo ${this.edad} años.`); 14 } 15} 16 17let persona1 = new Persona("Carlos", 30, false); 18persona1.saludar(); // "Hola, me llamo Carlos y tengo 30 años."Salida de consola Hola, me llamo Carlos y tengo 30 años.
Clases con métodos:
1class Coche { 2 marca: string; 3 modelo: string; 4 año: number; 5 6 constructor(marca: string, modelo: string, año: number) { 7 this.marca = marca; 8 this.modelo = modelo; 9 this.año = año; 10 } 11 12 mostrarInfo(): void { 13 console.log(`${this.marca} ${this.modelo} (${this.año})`); 14 } 15 16 actualizarAño(nuevoAño: number): void { 17 this.año = nuevoAño; 18 } 19} 20 21let miCoche = new Coche("Ford", "Mustang", 2020); 22miCoche.mostrarInfo(); // "Ford Mustang (2020)" 23miCoche.actualizarAño(2021); 24miCoche.mostrarInfo(); // "Ford Mustang (2021)"Salida de consola Ford Mustang (2020) Ford Mustang (2021)
Modificadores de acceso: public, private, protected #
TypeScript proporciona modificadores de acceso que controlan la visibilidad de las propiedades y métodos dentro de una clase. Esto es útil para encapsular y proteger los datos internos de una clase.
public: Las propiedades y métodos son accesibles desde cualquier parte. Es el modificador por defecto.private: Las propiedades y métodos solo son accesibles dentro de la clase donde se definen.protected: Similar aprivate, pero permite el acceso en clases derivadas (herencia).
Ejemplo de modificadores de acceso:
1class CuentaBancaria { 2 public titular: string; 3 private saldo: number; 4 protected numeroCuenta: string; 5 6 constructor(titular: string, saldo: number, numeroCuenta: string) { 7 this.titular = titular; 8 this.saldo = saldo; 9 this.numeroCuenta = numeroCuenta; 10 } 11 12 public depositar(cantidad: number): void { 13 this.saldo += cantidad; 14 console.log(`Depósito realizado. Saldo actual: ${this.saldo}`); 15 } 16 17 private retirar(cantidad: number): void { 18 if (cantidad > this.saldo) { 19 console.log("Saldo insuficiente."); 20 } else { 21 this.saldo -= cantidad; 22 console.log(`Retiro realizado. Saldo actual: ${this.saldo}`); 23 } 24 } 25 26 protected mostrarSaldo(): void { 27 console.log(`Saldo: ${this.saldo}`); 28 } 29} 30 31let cuenta = new CuentaBancaria("Laura", 1000, "123456789"); 32// Acceso público 33console.log(cuenta.titular); // "Laura" 34cuenta.depositar(500); // "Depósito realizado. Saldo actual: 1500" 35 36// Acceso privado 37console.log(cuenta.saldo); // <-- Error 38cuenta.retirar(200); // <-- Error 39 40// Acceso protegido 41console.log(cuenta.numeroCuenta); // <-- ErrorSalida de consola error TS2341: Property 'saldo' is private and only accessible within class 'CuentaBancaria'. error TS2341: Property 'retirar' is private and only accessible within class 'CuentaBancaria'. error TS2445: Property 'numeroCuenta' is protected and only accessible within class 'CuentaBancaria' and its subclasses.
Herencia con modificadores de acceso:
1class CuentaBancaria { 2 public titular: string; 3 private saldo: number; 4 protected numeroCuenta: string; 5 6 constructor(titular: string, saldo: number, numeroCuenta: string) { 7 this.titular = titular; 8 this.saldo = saldo; 9 this.numeroCuenta = numeroCuenta; 10 } 11 12 public depositar(cantidad: number): void { 13 this.saldo += cantidad; 14 console.log(`Depósito realizado. Saldo actual: ${this.saldo}`); 15 } 16 17 private retirar(cantidad: number): void { 18 if (cantidad > this.saldo) { 19 console.log("Saldo insuficiente."); 20 } else { 21 this.saldo -= cantidad; 22 console.log(`Retiro realizado. Saldo actual: ${this.saldo}`); 23 } 24 } 25 26 protected mostrarSaldo(): void { 27 console.log(`Saldo: ${this.saldo}`); 28 } 29} 30 31class CuentaAhorros extends CuentaBancaria { 32 constructor(titular: string, saldo: number, numeroCuenta: string) { 33 super(titular, saldo, numeroCuenta); 34 } 35 36 public mostrarDetalleCuenta(): void { 37 // Acceso a propiedad protegida 38 console.log(`Número de Cuenta: ${this.numeroCuenta}`); 39 // Acceso a método protegido 40 this.mostrarSaldo(); 41 } 42} 43 44let cuentaAhorros = new CuentaAhorros("Pedro", 2000, "987654321"); 45cuentaAhorros.depositar(1000); // "Depósito realizado. Saldo actual: 3000" 46cuentaAhorros.mostrarDetalleCuenta(); // "Número de Cuenta: 987654321" y "Saldo: 3000"Salida de consola Depósito realizado. Saldo actual: 3000 Número de Cuenta: 987654321 Saldo: 3000
Herencia y polimorfismo #
La herencia permite crear nuevas clases basadas en clases existentes, reutilizando y extendiendo su funcionalidad. El polimorfismo permite que una misma interfaz sea utilizada para diferentes tipos de datos, facilitando la flexibilidad y escalabilidad del código.
Ejemplo de herencia:
1class Animal { 2 nombre: string; 3 constructor(nombre: string) { 4 this.nombre = nombre; 5 } 6 hacerSonido(): void { console.log(`${this.nombre} hace un sonido genérico.`); } 7} 8 9class Perro extends Animal { 10 constructor(nombre: string) { 11 super(nombre); 12 } 13 hacerSonido(): void { console.log(`${this.nombre} ladra.`); } 14} 15 16class Gato extends Animal { 17 constructor(nombre: string) { 18 super(nombre); 19 } 20 hacerSonido(): void { console.log(`${this.nombre} maúlla.`); } 21} 22 23let miPerro = new Perro("Rex"); 24let miGato = new Gato("Mia"); 25 26miPerro.hacerSonido(); // "Rex ladra." 27miGato.hacerSonido(); // "Mia maúlla."Salida de consola Rex ladra. Mia maúlla.
Polimorfismo mediante métodos sobrescritos:
En el ejemplo anterior, hacerSonido es un método que se comporta de manera diferente dependiendo de la clase que lo implemente (Perro o Gato), demostrando el polimorfismo.
Uso de polimorfismo con arrays:
1class Animal { 2 nombre: string; 3 constructor(nombre: string) { 4 this.nombre = nombre; 5 } 6 hacerSonido(): void { console.log(`${this.nombre} hace un sonido genérico.`); } 7} 8 9class Perro extends Animal { 10 constructor(nombre: string) { 11 super(nombre); 12 } 13 hacerSonido(): void { console.log(`${this.nombre} ladra.`); } 14} 15 16class Gato extends Animal { 17 constructor(nombre: string) { 18 super(nombre); 19 } 20 hacerSonido(): void { console.log(`${this.nombre} maúlla.`); } 21} 22 23let animales: Animal[] = [new Perro("Rex"), new Gato("Mia"), new Animal("Genérico")]; 24 25animales.forEach(animal => animal.hacerSonido());Salida de consola Rex ladra. Mia maúlla Genérico hace un sonido genérico.
Aquí, aunque todos los elementos son del tipo Animal, cada uno ejecuta su propia versión del método hacerSonido.
Interfaces y clases abstractas #
Las clases abstractas y las interfaces son herramientas poderosas para definir contratos y estructuras que las clases deben seguir. Las clases abstractas pueden contener implementaciones parciales, mientras que las interfaces solo definen la estructura sin implementación.
Interfaces vs Clases Abstractas:
-
Interfaces:
- Solo definen la forma de un objeto (propiedades y métodos).
- No pueden contener implementación de métodos.
- Una clase puede implementar múltiples interfaces.
-
Clases Abstractas:
- Pueden contener tanto definiciones como implementaciones de métodos.
- No pueden ser instanciadas directamente.
- Una clase puede heredar de una sola clase abstracta.
Ejemplo de interfaz:
1interface Volador { 2 volar(): void; 3} 4 5interface Nadador { 6 nadar(): void; 7} 8 9class Pato implements Volador, Nadador { 10 volar(): void { 11 console.log("El pato está volando."); 12 } 13 14 nadar(): void { 15 console.log("El pato está nadando."); 16 } 17} 18 19let pato = new Pato(); 20pato.volar(); // "El pato está volando." 21pato.nadar(); // "El pato está nadando."Salida de consola El pato está volando. El pato está nadando.
Ejemplo de clase abstracta:
1abstract class Vehiculo { 2 marca: string; 3 4 constructor(marca: string) { 5 this.marca = marca; 6 } 7 8 abstract arrancar(): void; // Método abstracto sin implementación 9 10 detener(): void { 11 console.log(`${this.marca} se ha detenido.`); 12 } 13} 14 15class Coche extends Vehiculo { 16 constructor(marca: string) { 17 super(marca); 18 } 19 20 arrancar(): void { 21 console.log(`${this.marca} está arrancando.`); 22 } 23} 24 25let miCoche = new Coche("BMW"); 26miCoche.arrancar(); // "BMW está arrancando." 27miCoche.detener(); // "BMW se ha detenido."Salida de consola BMW está arrancando. BMW se ha detenido.
Clases abstractas con métodos implementados y abstractos:
1abstract class Figura { 2 abstract area(): number; // Método abstracto 3 4 descripcion(): void { 5 console.log("Esta es una figura geométrica."); 6 } 7} 8 9class Circulo extends Figura { 10 radio: number; 11 12 constructor(radio: number) { 13 super(); 14 this.radio = radio; 15 } 16 17 area(): number { 18 return Math.PI * this.radio * this.radio; 19 } 20} 21 22let circulo = new Circulo(5); 23circulo.descripcion(); // "Esta es una figura geométrica." 24console.log(`Área del círculo: ${circulo.area()}`); // "Área del círculo: 78.53981633974483"Salida de consola Esta es una figura geométrica. Área del círculo: 78.53981633974483
Uso combinado de interfaces y clases abstractas:
1interface Volador { 2 volar(): void; 3} 4 5abstract class Ave implements Volador { 6 nombre: string; 7 8 constructor(nombre: string) { 9 this.nombre = nombre; 10 } 11 12 abstract volar(): void; 13 14 cantar(): void { 15 console.log(`${this.nombre} está cantando.`); 16 } 17} 18 19class Aguila extends Ave { 20 constructor(nombre: string) { 21 super(nombre); 22 } 23 24 volar(): void { 25 console.log(`${this.nombre} está volando alto.`); 26 } 27} 28 29let aguila = new Aguila("Águila Real"); 30aguila.cantar(); // "Águila Real está cantando." 31aguila.volar(); // "Águila Real está volando alto."Salida de consola Águila Real está cantando. Águila Real está volando alto.
Resumen del tema
Conceptos clave #
- Tipado de objetos e interfaces: definición de contratos con
interface, propiedades opcionales (?), de solo lectura (readonly) y métodos tipados. - Programación Orientada a Objetos: estructura de clases con campos, constructor (
constructor), métodos e instanciación connew. - Modificadores de acceso: control de encapsulamiento mediante
public(acceso global),private(restringido a la clase) yprotected(accesible en la clase y subclases derivadas). - Herencia y polimorfismo: extensión de clases base (
extends), invocación del constructor padre consuper()y sobrescritura de métodos para respuestas polimórficas. - Clases abstractas vs Interfaces: clases abstractas (
abstract class) como plantillas base con lógica compartida y métodos abstractos por implementar frente a interfaces que definen contratos estructurales puros sin implementación (implements).
Qué debes recordar #
Usa interface para definir la forma de los datos y APIs externas, y utiliza class con modificadores de acceso (private/protected) y clases abstractas para encapsular lógica de negocio compleja.