Modificadores public, private y protected
Los modificadores de acceso son una característica fundamental en TypeScript que nos permite controlar la visibilidad y accesibilidad de las propiedades y métodos de una clase. Estos modificadores son esenciales para implementar la encapsulación, uno de los pilares de la programación orientada a objetos.
TypeScript ofrece tres modificadores de acceso principales: public
, private
y protected
. Cada uno define un nivel diferente de accesibilidad, permitiéndonos diseñar clases con interfaces bien definidas y ocultar los detalles de implementación.
Modificador public
El modificador public
es el nivel de acceso predeterminado en TypeScript. Cuando una propiedad o método se declara como público, puede ser accedido desde cualquier parte del código, sin restricciones.
class User {
public username: string;
constructor(username: string) {
this.username = username;
}
public displayInfo(): void {
console.log(`Username: ${this.username}`);
}
}
const user = new User("john_doe");
console.log(user.username); // Acceso permitido
user.displayInfo(); // Acceso permitido
En este ejemplo, tanto la propiedad username
como el método displayInfo()
son públicos, por lo que pueden ser accedidos directamente desde fuera de la clase. Al ser public
el modificador predeterminado, podríamos omitirlo y el comportamiento sería el mismo.
Modificador private
El modificador private
restringe el acceso a propiedades y métodos para que solo sean accesibles dentro de la misma clase donde se declaran. Esto es útil para ocultar detalles de implementación interna.
class BankAccount {
private balance: number;
private accountNumber: string;
constructor(initialBalance: number, accountNumber: string) {
this.balance = initialBalance;
this.accountNumber = accountNumber;
}
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
this.logTransaction("deposit", amount);
}
}
public withdraw(amount: number): boolean {
if (amount > 0 && this.balance >= amount) {
this.balance -= amount;
this.logTransaction("withdrawal", amount);
return true;
}
return false;
}
public getBalance(): number {
return this.balance;
}
private logTransaction(type: string, amount: number): void {
console.log(`Transaction: ${type} of $${amount} from account ${this.accountNumber}`);
}
}
const account = new BankAccount(1000, "ACC123456");
// Acceso a métodos públicos
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
// Estos accesos generarían errores de compilación:
// console.log(account.balance); // Error: 'balance' es privado
// console.log(account.accountNumber); // Error: 'accountNumber' es privado
// account.logTransaction("test", 100); // Error: 'logTransaction' es privado
En este ejemplo, balance
y accountNumber
son propiedades privadas, lo que significa que no se puede acceder a ellas directamente desde fuera de la clase. El método logTransaction()
también es privado, por lo que solo puede ser llamado desde otros métodos dentro de la clase.
Modificador protected
El modificador protected
es un punto intermedio entre public
y private
. Los miembros protected
son accesibles dentro de la clase donde se declaran y en las clases que heredan de ella, pero no desde fuera.
class Person {
protected name: string;
protected age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
protected getDetails(): string {
return `${this.name}, ${this.age} years old`;
}
}
class Employee extends Person {
private department: string;
constructor(name: string, age: number, department: string) {
super(name, age);
this.department = department;
}
public getEmployeeInfo(): string {
// Podemos acceder a propiedades y métodos protected de la clase padre
return `${this.getDetails()} - Department: ${this.department}`;
}
public updateName(newName: string): void {
// Podemos modificar propiedades protected heredadas
this.name = newName;
}
}
const employee = new Employee("Alice", 30, "Engineering");
console.log(employee.getEmployeeInfo()); // "Alice, 30 years old - Department: Engineering"
employee.updateName("Alicia");
console.log(employee.getEmployeeInfo()); // "Alicia, 30 years old - Department: Engineering"
// Estos accesos generarían errores:
// console.log(employee.name); // Error: 'name' es protected
// console.log(employee.age); // Error: 'age' es protected
// employee.getDetails(); // Error: 'getDetails' es protected
En este ejemplo, name
y age
son propiedades protegidas en la clase Person
. La clase Employee
, que hereda de Person
, puede acceder a estas propiedades y al método protegido getDetails()
. Sin embargo, desde fuera de estas clases, no se puede acceder directamente a estos miembros protegidos.
Declaración abreviada en el constructor
TypeScript ofrece una sintaxis abreviada para declarar propiedades de clase y asignarlas a través del constructor en una sola línea:
class Product {
constructor(
public id: string,
private name: string,
private price: number,
protected category: string
) {}
public getProductInfo(): string {
return `Product: ${this.name}, Price: $${this.price}, Category: ${this.category}`;
}
}
const laptop = new Product("P001", "Laptop", 999.99, "Electronics");
console.log(laptop.getProductInfo()); // "Product: Laptop, Price: $999.99, Category: Electronics"
console.log(laptop.id); // "P001" - accesible porque es public
// Estos accesos generarían errores:
// console.log(laptop.name); // Error: 'name' es private
// console.log(laptop.price); // Error: 'price' es private
// console.log(laptop.category); // Error: 'category' es protected
Esta sintaxis elimina la necesidad de declarar las propiedades por separado y luego asignarlas en el constructor, lo que hace que el código sea más conciso y legible.
Aplicaciones prácticas de los modificadores de acceso
Los modificadores de acceso son fundamentales para implementar patrones de diseño y mantener un código robusto:
- Seguridad de datos: Usar
private
para propiedades que almacenan datos sensibles evita modificaciones accidentales.
class UserCredentials {
constructor(
public username: string,
private password: string
) {}
public authenticate(inputPassword: string): boolean {
return this.password === inputPassword;
}
// No hay método para obtener la contraseña directamente
}
const credentials = new UserCredentials("admin", "secret123");
console.log(credentials.authenticate("secret123")); // true
// console.log(credentials.password); // Error: 'password' es private
- API pública clara: Marcar como
public
solo lo que debe ser parte de la API pública de la clase.
class DataProcessor {
constructor(private data: number[]) {}
public process(): number {
return this.calculateAverage() * this.getNormalizationFactor();
}
private calculateAverage(): number {
const sum = this.data.reduce((acc, val) => acc + val, 0);
return sum / this.data.length;
}
private getNormalizationFactor(): number {
return this.data.length > 10 ? 1.5 : 1;
}
}
const processor = new DataProcessor([1, 2, 3, 4, 5]);
console.log(processor.process()); // API pública clara
// processor.calculateAverage(); // Error: método privado
- Jerarquías de clases: Usar
protected
para compartir funcionalidad entre clases relacionadas sin exponerla públicamente.
class Shape {
protected calculateArea(): number {
return 0; // Implementación base
}
public getDescription(): string {
return `This shape has an area of ${this.calculateArea()} square units.`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
protected calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
const circle = new Circle(5);
console.log(circle.getDescription()); // "This shape has an area of 78.54... square units."
// circle.calculateArea(); // Error: método protegido
Los modificadores de acceso son herramientas poderosas para estructurar nuestro código de manera que refleje claramente nuestras intenciones de diseño, proteja los datos y facilite el mantenimiento a largo plazo.
¿Te está gustando esta lección?
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
Principios de encapsulación en la práctica
La encapsulación es uno de los pilares fundamentales de la programación orientada a objetos que permite ocultar los detalles internos de implementación de una clase y exponer solo lo necesario. En TypeScript, implementamos la encapsulación principalmente a través de los modificadores de acceso, pero el concepto va más allá de simplemente marcar propiedades como privadas.
Una buena encapsulación nos permite crear abstracciones robustas que son más fáciles de mantener, extender y utilizar correctamente. Veamos cómo aplicar estos principios en situaciones prácticas.
Ocultamiento de la implementación interna
El primer principio de la encapsulación consiste en ocultar los detalles de implementación que no necesitan ser expuestos:
class ShoppingCart {
private items: Array<{product: string, price: number, quantity: number}> = [];
private discountCode: string | null = null;
public addItem(product: string, price: number, quantity: number = 1): void {
this.items.push({ product, price, quantity });
}
public removeItem(index: number): void {
if (index >= 0 && index < this.items.length) {
this.items.splice(index, 1);
}
}
public applyDiscount(code: string): void {
this.discountCode = code;
}
public getTotalPrice(): number {
const subtotal = this.calculateSubtotal();
return this.applyDiscountToTotal(subtotal);
}
private calculateSubtotal(): number {
return this.items.reduce((total, item) =>
total + (item.price * item.quantity), 0);
}
private applyDiscountToTotal(amount: number): number {
if (this.discountCode === "SAVE10") {
return amount * 0.9; // 10% discount
}
return amount;
}
}
En este ejemplo, el usuario del carrito de compras no necesita saber cómo se calculan los precios internamente o cómo se almacenan los productos. Solo necesita una interfaz clara para agregar/eliminar productos y obtener el precio total.
Validación de datos de entrada
La encapsulación nos permite validar los datos antes de modificar el estado interno:
class BankAccount {
private _balance: number = 0;
private _accountNumber: string;
private _dailyWithdrawalLimit: number = 1000;
private _withdrawalsToday: number = 0;
constructor(accountNumber: string, initialBalance: number = 0) {
if (accountNumber.length !== 10) {
throw new Error("Account number must be 10 characters long");
}
if (initialBalance < 0) {
throw new Error("Initial balance cannot be negative");
}
this._accountNumber = accountNumber;
this._balance = initialBalance;
}
public deposit(amount: number): boolean {
if (amount <= 0) {
console.error("Deposit amount must be positive");
return false;
}
this._balance += amount;
return true;
}
public withdraw(amount: number): boolean {
if (amount <= 0) {
console.error("Withdrawal amount must be positive");
return false;
}
if (amount > this._balance) {
console.error("Insufficient funds");
return false;
}
if (this._withdrawalsToday >= 3) {
console.error("Daily withdrawal limit reached");
return false;
}
if (amount > this._dailyWithdrawalLimit) {
console.error(`Cannot withdraw more than ${this._dailyWithdrawalLimit} per day`);
return false;
}
this._balance -= amount;
this._withdrawalsToday++;
return true;
}
public getBalance(): number {
return this._balance;
}
}
Aquí, la encapsulación nos permite implementar reglas de negocio complejas (límites de retiro, validación de números de cuenta) sin exponer estos detalles al usuario de la clase.
Mantenimiento del estado consistente
La encapsulación nos ayuda a mantener el estado interno consistente, asegurando que las propiedades relacionadas siempre estén sincronizadas:
class Rectangle {
private _width: number;
private _height: number;
private _area: number;
private _perimeter: number;
constructor(width: number, height: number) {
this._width = width;
this._height = height;
this.recalculate();
}
private recalculate(): void {
this._area = this._width * this._height;
this._perimeter = 2 * (this._width + this._height);
}
public get width(): number {
return this._width;
}
public set width(value: number) {
if (value <= 0) {
throw new Error("Width must be positive");
}
this._width = value;
this.recalculate();
}
public get height(): number {
return this._height;
}
public set height(value: number) {
if (value <= 0) {
throw new Error("Height must be positive");
}
this._height = value;
this.recalculate();
}
public get area(): number {
return this._area;
}
public get perimeter(): number {
return this._perimeter;
}
}
En este ejemplo, el área y el perímetro se recalculan automáticamente cuando cambia el ancho o el alto, manteniendo el estado interno consistente sin que el usuario tenga que preocuparse por ello.
Abstracción de complejidad
La encapsulación nos permite ocultar operaciones complejas detrás de interfaces simples:
class ImageProcessor {
private image: number[][]; // Representación matricial de la imagen
constructor(imageData: number[][]) {
this.image = imageData;
}
public applyFilter(filterName: string): void {
switch (filterName) {
case "blur":
this.applyBlurFilter();
break;
case "sharpen":
this.applySharpenFilter();
break;
case "grayscale":
this.applyGrayscaleFilter();
break;
default:
throw new Error(`Unknown filter: ${filterName}`);
}
}
private applyBlurFilter(): void {
// Implementación compleja del algoritmo de desenfoque
console.log("Applying blur filter...");
// Código para aplicar el filtro...
}
private applySharpenFilter(): void {
// Implementación compleja del algoritmo de nitidez
console.log("Applying sharpen filter...");
// Código para aplicar el filtro...
}
private applyGrayscaleFilter(): void {
// Implementación compleja del algoritmo de escala de grises
console.log("Applying grayscale filter...");
// Código para aplicar el filtro...
}
public getProcessedImage(): number[][] {
return this.image;
}
}
El usuario solo necesita conocer los nombres de los filtros disponibles, sin preocuparse por los complejos algoritmos de procesamiento de imágenes que se ejecutan internamente.
Cambios de implementación sin afectar la interfaz
La encapsulación nos permite cambiar la implementación interna sin afectar el código que utiliza nuestra clase:
class UserAuthentication {
private users: Map<string, { passwordHash: string, salt: string }>;
constructor() {
this.users = new Map();
}
public registerUser(username: string, password: string): boolean {
if (this.users.has(username)) {
return false;
}
// Versión 1: Almacenamiento simple (podría cambiar en el futuro)
const salt = this.generateSalt();
const passwordHash = this.hashPassword(password, salt);
this.users.set(username, { passwordHash, salt });
return true;
}
public authenticateUser(username: string, password: string): boolean {
const user = this.users.get(username);
if (!user) {
return false;
}
const hashedInput = this.hashPassword(password, user.salt);
return hashedInput === user.passwordHash;
}
private generateSalt(): string {
// En una implementación real, generaríamos un salt aleatorio
return Math.random().toString(36).substring(2, 15);
}
private hashPassword(password: string, salt: string): string {
// En una implementación real, usaríamos un algoritmo de hash seguro
// Esta es solo una simulación simple
return `${password}:${salt}`;
}
}
Si más adelante decidimos cambiar el algoritmo de hash o la estructura de almacenamiento de usuarios, podemos hacerlo sin afectar el código que utiliza esta clase, siempre que mantengamos la misma interfaz pública.
Patrones de diseño basados en encapsulación
La encapsulación es la base de muchos patrones de diseño. Por ejemplo, el patrón Singleton garantiza que una clase tenga una única instancia y proporciona un punto de acceso global a ella:
class DatabaseConnection {
private static instance: DatabaseConnection | null = null;
private connectionString: string;
private isConnected: boolean = false;
private constructor(connectionString: string) {
this.connectionString = connectionString;
}
public static getInstance(connectionString: string): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection(connectionString);
}
return DatabaseConnection.instance;
}
public connect(): boolean {
if (this.isConnected) {
return true;
}
console.log(`Connecting to database with: ${this.connectionString}`);
// Lógica de conexión real aquí...
this.isConnected = true;
return true;
}
public query(sql: string): any[] {
if (!this.isConnected) {
throw new Error("Must connect to database before querying");
}
console.log(`Executing query: ${sql}`);
// Lógica de consulta real aquí...
return [];
}
public disconnect(): void {
if (this.isConnected) {
console.log("Disconnecting from database");
this.isConnected = false;
}
}
}
// Uso del singleton
const db1 = DatabaseConnection.getInstance("server=localhost;user=root");
const db2 = DatabaseConnection.getInstance("server=localhost;user=root");
console.log(db1 === db2); // true - ambas variables referencian la misma instancia
db1.connect();
db1.query("SELECT * FROM users");
En este ejemplo, el constructor privado y el método estático getInstance()
encapsulan la lógica de creación de instancias, asegurando que solo exista una conexión a la base de datos.
Beneficios prácticos de la encapsulación
-
Reducción de dependencias: Al ocultar los detalles de implementación, reducimos el acoplamiento entre componentes.
-
Código más mantenible: Los cambios internos no afectan al código que utiliza la clase.
-
Mayor seguridad: Controlamos cómo se accede y modifica el estado interno.
-
Mejor testabilidad: Podemos probar la interfaz pública sin preocuparnos por los detalles internos.
-
Evolución del código: Podemos mejorar la implementación interna sin romper el código existente.
La encapsulación no es solo una regla técnica, sino un principio de diseño que nos ayuda a crear código más robusto, flexible y fácil de mantener a largo plazo. Aplicando estos principios en la práctica, podemos construir sistemas complejos a partir de componentes bien encapsulados que funcionan juntos de manera predecible y confiable.
Patrón de acceso mediante getters y setters
Los getters y setters son métodos especiales que proporcionan una forma controlada de acceder y modificar las propiedades de una clase. Este patrón es fundamental para implementar la encapsulación en TypeScript, permitiéndonos ocultar los detalles internos de implementación mientras exponemos una interfaz clara para interactuar con los objetos.
TypeScript ofrece dos formas principales de implementar getters y setters: la sintaxis tradicional de métodos y la sintaxis de accesores de propiedades. Ambas cumplen el mismo propósito, pero con diferentes estilos de código y casos de uso.
Implementación básica con métodos tradicionales
La forma más directa de implementar getters y setters es mediante métodos explícitos:
class Person {
private _firstName: string;
private _lastName: string;
private _age: number;
constructor(firstName: string, lastName: string, age: number) {
this._firstName = firstName;
this._lastName = lastName;
this._age = age;
}
// Getters
public getFirstName(): string {
return this._firstName;
}
public getLastName(): string {
return this._lastName;
}
public getAge(): number {
return this._age;
}
// Setters
public setFirstName(firstName: string): void {
this._firstName = firstName;
}
public setLastName(lastName: string): void {
this._lastName = lastName;
}
public setAge(age: number): void {
if (age < 0 || age > 120) {
throw new Error("Age must be between 0 and 120");
}
this._age = age;
}
// Método adicional que utiliza propiedades privadas
public getFullName(): string {
return `${this._firstName} ${this._lastName}`;
}
}
const person = new Person("John", "Doe", 30);
console.log(person.getFullName()); // "John Doe"
person.setAge(31);
console.log(person.getAge()); // 31
En este ejemplo, las propiedades _firstName
, _lastName
y _age
son privadas (indicado por el prefijo _
y el modificador private
). Para acceder a ellas desde fuera de la clase, utilizamos métodos getter y setter específicos.
Sintaxis de accesores de propiedades
TypeScript también admite una sintaxis más moderna y concisa para getters y setters, utilizando las palabras clave get
y set
:
class Product {
private _name: string;
private _price: number;
private _stock: number;
constructor(name: string, price: number, stock: number) {
this._name = name;
this._price = price;
this._stock = stock;
}
// Getter para name
get name(): string {
return this._name;
}
// Setter para name
set name(value: string) {
if (value.trim() === "") {
throw new Error("Product name cannot be empty");
}
this._name = value;
}
// Getter para price
get price(): number {
return this._price;
}
// Setter para price
set price(value: number) {
if (value < 0) {
throw new Error("Price cannot be negative");
}
this._price = value;
}
// Getter para stock
get stock(): number {
return this._stock;
}
// Setter para stock
set stock(value: number) {
if (!Number.isInteger(value) || value < 0) {
throw new Error("Stock must be a non-negative integer");
}
this._stock = value;
}
// Propiedad calculada (solo getter)
get isAvailable(): boolean {
return this._stock > 0;
}
}
const laptop = new Product("Laptop", 999.99, 5);
// Uso de getters
console.log(laptop.name); // "Laptop"
console.log(laptop.price); // 999.99
console.log(laptop.isAvailable); // true
// Uso de setters
laptop.price = 899.99;
laptop.stock = 3;
// Validación en acción
try {
laptop.price = -50; // Lanzará un error
} catch (error) {
console.error(error.message); // "Price cannot be negative"
}
Con esta sintaxis, los getters y setters se utilizan como si fueran propiedades normales, lo que hace que el código sea más limpio y natural. Internamente, sin embargo, se están ejecutando los métodos definidos.
Ventajas de usar getters y setters
- 1. Validación de datos: Podemos validar los valores antes de asignarlos a las propiedades.
class Temperature {
private _celsius: number;
constructor(celsius: number) {
this.celsius = celsius; // Usa el setter para validación
}
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) {
throw new Error("Temperature cannot be below absolute zero (-273.15°C)");
}
this._celsius = value;
}
get fahrenheit(): number {
return (this._celsius * 9/5) + 32;
}
set fahrenheit(value: number) {
// Convertir Fahrenheit a Celsius y usar el setter de celsius para validación
this.celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature(25);
console.log(temp.fahrenheit); // 77
temp.celsius = 100;
console.log(temp.fahrenheit); // 212
temp.fahrenheit = 68;
console.log(temp.celsius); // 20
- 2. Propiedades calculadas: Podemos crear propiedades que se calculan dinámicamente.
class Circle {
private _radius: number;
constructor(radius: number) {
this._radius = radius;
}
get radius(): number {
return this._radius;
}
set radius(value: number) {
if (value <= 0) {
throw new Error("Radius must be positive");
}
this._radius = value;
}
get diameter(): number {
return this._radius * 2;
}
get area(): number {
return Math.PI * this._radius * this._radius;
}
get circumference(): number {
return 2 * Math.PI * this._radius;
}
}
const circle = new Circle(5);
console.log(circle.area); // ~78.54
console.log(circle.circumference); // ~31.42
circle.radius = 10;
console.log(circle.diameter); // 20
- 3. Control de acceso asimétrico: Podemos permitir leer una propiedad pero no modificarla (o viceversa).
class User {
private _username: string;
private _createdAt: Date;
private _password: string;
constructor(username: string, password: string) {
this._username = username;
this._password = this.hashPassword(password);
this._createdAt = new Date();
}
// Getter sin setter - propiedad de solo lectura
get username(): string {
return this._username;
}
// Getter sin setter - propiedad de solo lectura
get createdAt(): Date {
return new Date(this._createdAt.getTime()); // Devuelve una copia para evitar modificaciones
}
// Solo setter sin getter - no se puede leer directamente
set password(newPassword: string) {
if (newPassword.length < 8) {
throw new Error("Password must be at least 8 characters long");
}
this._password = this.hashPassword(newPassword);
}
// Método para verificar la contraseña
verifyPassword(password: string): boolean {
return this.hashPassword(password) === this._password;
}
private hashPassword(password: string): string {
// En una aplicación real, usaríamos un algoritmo de hash seguro
return `hashed_${password}`;
}
}
const user = new User("john_doe", "securepass123");
console.log(user.username); // "john_doe"
console.log(user.createdAt); // Date object
// No podemos hacer esto:
// user.username = "new_username"; // Error: solo lectura
// console.log(user.password); // Error: no hay getter
// Cambiar contraseña
user.password = "newSecurePass456";
console.log(user.verifyPassword("newSecurePass456")); // true
- 4. Lógica de cambio de estado: Podemos ejecutar lógica adicional cuando una propiedad cambia.
class TodoItem {
private _title: string;
private _completed: boolean;
private _completedAt: Date | null;
constructor(title: string) {
this._title = title;
this._completed = false;
this._completedAt = null;
}
get title(): string {
return this._title;
}
set title(value: string) {
if (value.trim() === "") {
throw new Error("Title cannot be empty");
}
this._title = value;
}
get completed(): boolean {
return this._completed;
}
set completed(value: boolean) {
// Si cambiamos de incompleto a completo
if (value === true && this._completed === false) {
this._completedAt = new Date();
}
// Si cambiamos de completo a incompleto
if (value === false && this._completed === true) {
this._completedAt = null;
}
this._completed = value;
}
get completedAt(): Date | null {
return this._completedAt;
}
}
const task = new TodoItem("Learn TypeScript");
console.log(task.completed); // false
console.log(task.completedAt); // null
task.completed = true;
console.log(task.completedAt); // Date object
task.completed = false;
console.log(task.completedAt); // null
Patrones avanzados con getters y setters
- Propiedades con memoria (memoization): Podemos calcular un valor solo cuando sea necesario y almacenarlo en caché.
class DataProcessor {
private _data: number[];
private _sortedData: number[] | null = null;
private _statistics: { mean: number; median: number } | null = null;
constructor(data: number[]) {
this._data = [...data]; // Copia para evitar modificaciones externas
}
get data(): number[] {
return [...this._data]; // Devuelve una copia para evitar modificaciones
}
set data(newData: number[]) {
this._data = [...newData];
// Invalidar caché cuando los datos cambian
this._sortedData = null;
this._statistics = null;
}
get sortedData(): number[] {
// Calcular solo si es necesario
if (this._sortedData === null) {
this._sortedData = [...this._data].sort((a, b) => a - b);
}
return [...this._sortedData];
}
get statistics(): { mean: number; median: number } {
// Calcular solo si es necesario
if (this._statistics === null) {
const mean = this._data.reduce((sum, val) => sum + val, 0) / this._data.length;
// Para la mediana, necesitamos los datos ordenados
const sorted = this.sortedData; // Usa el getter que implementa memoization
const mid = Math.floor(sorted.length / 2);
const median = sorted.length % 2 === 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
this._statistics = { mean, median };
}
return { ...this._statistics };
}
}
const processor = new DataProcessor([5, 1, 9, 3, 7]);
console.log(processor.statistics); // Calcula y almacena en caché
console.log(processor.statistics); // Usa el valor en caché
processor.data = [10, 20, 30, 40];
console.log(processor.statistics); // Recalcula porque los datos cambiaron
- Propiedades proxy: Podemos usar getters y setters para acceder a propiedades de objetos anidados.
class UserProfile {
private _user: {
id: number;
personal: {
firstName: string;
lastName: string;
};
contact: {
email: string;
phone: string | null;
};
};
constructor(
id: number,
firstName: string,
lastName: string,
email: string,
phone: string | null = null
) {
this._user = {
id,
personal: { firstName, lastName },
contact: { email, phone }
};
}
// Getters y setters para propiedades anidadas
get firstName(): string {
return this._user.personal.firstName;
}
set firstName(value: string) {
this._user.personal.firstName = value;
}
get lastName(): string {
return this._user.personal.lastName;
}
set lastName(value: string) {
this._user.personal.lastName = value;
}
get email(): string {
return this._user.contact.email;
}
set email(value: string) {
if (!value.includes('@')) {
throw new Error("Invalid email format");
}
this._user.contact.email = value;
}
get phone(): string | null {
return this._user.contact.phone;
}
set phone(value: string | null) {
this._user.contact.phone = value;
}
// Propiedad calculada
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
// Método para obtener todos los datos
getData(): any {
return { ...this._user }; // Devuelve una copia
}
}
const profile = new UserProfile(1, "Jane", "Smith", "jane@example.com");
console.log(profile.fullName); // "Jane Smith"
profile.lastName = "Johnson";
profile.email = "jane.johnson@example.com";
console.log(profile.getData());
Mejores prácticas para getters y setters
- 1. Convenciones de nomenclatura: Usa un prefijo de guion bajo para propiedades privadas.
// Buena práctica
private _count: number;
get count(): number { return this._count; }
// Evitar
private count: number; // Confuso con el getter
- 2. Operaciones ligeras en getters: Los getters deben ser rápidos y no tener efectos secundarios.
// Buena práctica
get fullName(): string {
return `${this._firstName} ${this._lastName}`;
}
// Evitar en getters
get userData(): any {
// Evitar operaciones costosas o con efectos secundarios en getters
return this.fetchUserDataFromServer(); // ❌ Mal uso
}
- 3. Validación en setters: Aprovecha los setters para validar datos.
set email(value: string) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error("Invalid email format");
}
this._email = value;
}
- 4. Inmutabilidad: Devuelve copias de objetos y arrays para evitar modificaciones no controladas.
get items(): string[] {
return [...this._items]; // Devuelve una copia
}
get config(): object {
return { ...this._config }; // Copia superficial
}
- 5. Consistencia: Si una propiedad tiene getter, considera si también necesita un setter.
// Propiedad de solo lectura (tiene sentido)
get createdAt(): Date {
return new Date(this._createdAt.getTime());
}
// Propiedad calculada (tiene sentido que sea solo lectura)
get totalPrice(): number {
return this._items.reduce((sum, item) => sum + item.price, 0);
}
Los getters y setters son una herramienta poderosa para implementar la encapsulación en TypeScript. Permiten controlar el acceso a las propiedades de una clase, validar datos, implementar propiedades calculadas y mantener el estado interno consistente. Al utilizarlos correctamente, podemos crear interfaces de clase limpias y robustas que ocultan la complejidad interna mientras proporcionan una API intuitiva para los usuarios de nuestras clases.
Propiedades y métodos estáticos
Las propiedades y métodos estáticos son miembros de clase que pertenecen a la clase misma y no a las instancias individuales. Esto significa que puedes acceder a ellos directamente desde la clase sin necesidad de crear un objeto. Esta característica es especialmente útil para funcionalidades que son independientes del estado de instancias específicas.
En TypeScript, definimos miembros estáticos utilizando la palabra clave static
. Veamos cómo funcionan y cuáles son sus aplicaciones prácticas.
Definición básica de miembros estáticos
Para declarar una propiedad o método como estático, simplemente añadimos la palabra clave static
antes de su definición:
class MathOperations {
// Propiedad estática
static PI: number = 3.14159265359;
// Método estático
static square(num: number): number {
return num * num;
}
static cube(num: number): number {
return num * num * num;
}
}
// Acceso a miembros estáticos sin crear instancias
console.log(MathOperations.PI); // 3.14159265359
console.log(MathOperations.square(5)); // 25
console.log(MathOperations.cube(3)); // 27
En este ejemplo, tanto PI
como los métodos square
y cube
son estáticos. Podemos acceder a ellos directamente a través de la clase MathOperations
sin necesidad de crear una instancia.
Diferencias entre miembros estáticos y de instancia
Es importante entender la diferencia entre miembros estáticos y miembros de instancia:
class Counter {
// Propiedad estática - compartida entre todas las instancias
static totalCount: number = 0;
// Propiedad de instancia - única para cada instancia
instanceCount: number = 0;
constructor() {
// Incrementamos ambos contadores al crear una instancia
Counter.totalCount++;
this.instanceCount++;
}
// Método de instancia
increment(): void {
Counter.totalCount++;
this.instanceCount++;
}
// Método estático
static getTotalCount(): number {
return Counter.totalCount;
}
}
const counter1 = new Counter(); // totalCount = 1, instanceCount = 1
const counter2 = new Counter(); // totalCount = 2, instanceCount = 1
counter1.increment(); // totalCount = 3, instanceCount de counter1 = 2
counter2.increment(); // totalCount = 4, instanceCount de counter2 = 2
console.log(Counter.totalCount); // 4
console.log(counter1.instanceCount); // 2
console.log(counter2.instanceCount); // 2
console.log(Counter.getTotalCount()); // 4
En este ejemplo:
totalCount
es una propiedad estática compartida por todas las instancias deCounter
instanceCount
es una propiedad de instancia única para cada objeto- Cuando incrementamos el contador en una instancia, afecta a su propio
instanceCount
y altotalCount
compartido
Acceso a miembros estáticos desde dentro de la clase
Dentro de los métodos de clase, podemos acceder a los miembros estáticos de dos formas:
class Configuration {
static apiUrl: string = "https://api.example.com";
static timeout: number = 3000;
static version: string = "1.0.0";
// Método de instancia que accede a propiedades estáticas
getApiConfig(): object {
// Usando el nombre de la clase
return {
url: Configuration.apiUrl,
timeout: Configuration.timeout,
version: Configuration.version
};
}
// Método estático que accede a otras propiedades estáticas
static getFullUrl(endpoint: string): string {
// Usando this en contexto estático
return `${this.apiUrl}/${endpoint}?version=${this.version}`;
}
}
const config = new Configuration();
console.log(config.getApiConfig());
console.log(Configuration.getFullUrl("users"));
Observa que:
- En métodos de instancia, debemos usar el nombre de la clase para acceder a miembros estáticos
- En métodos estáticos, podemos usar
this
para referir a otros miembros estáticos
Casos de uso comunes para miembros estáticos
1. Constantes y configuración
Las propiedades estáticas son ideales para definir constantes o valores de configuración compartidos:
class AppSettings {
static readonly API_URL: string = "https://api.myapp.com/v1";
static readonly MAX_LOGIN_ATTEMPTS: number = 3;
static readonly DEFAULT_TIMEOUT: number = 5000;
static readonly SUPPORTED_LANGUAGES: string[] = ["en", "es", "fr", "de"];
// Podemos tener métodos estáticos para acceder a configuración
static isLanguageSupported(lang: string): boolean {
return AppSettings.SUPPORTED_LANGUAGES.includes(lang);
}
}
// Uso en la aplicación
if (AppSettings.isLanguageSupported("ja")) {
console.log("Japanese is supported");
} else {
console.log("Japanese is not supported");
}
const timeout = AppSettings.DEFAULT_TIMEOUT;
El uso de readonly
garantiza que estas constantes no puedan ser modificadas accidentalmente.
2. Métodos de utilidad
Los métodos estáticos son perfectos para funciones de utilidad que no dependen del estado de la instancia:
class StringUtils {
static capitalize(str: string): string {
if (!str) return str;
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
static truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + "...";
}
static slugify(str: string): string {
return str
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^\w\-]+/g, "");
}
}
// Uso
const title = "hello world";
console.log(StringUtils.capitalize(title)); // "Hello world"
console.log(StringUtils.truncate("This is a long text", 10)); // "This is a..."
console.log(StringUtils.slugify("Hello World!")); // "hello-world"
3. Fábricas y métodos de creación
Los métodos estáticos son útiles para implementar patrones de fábrica que crean instancias de una clase:
class User {
private constructor(
public readonly id: string,
public readonly name: string,
public readonly email: string,
private readonly createdAt: Date
) {}
// Método de fábrica para crear un nuevo usuario
static createNew(name: string, email: string): User {
const id = `user_${Date.now()}`;
return new User(id, name, email, new Date());
}
// Método de fábrica para crear desde datos existentes
static fromDatabase(data: any): User {
return new User(
data.id,
data.name,
data.email,
new Date(data.created_at)
);
}
// Método de fábrica para usuario invitado
static createGuest(): User {
return new User(
`guest_${Date.now()}`,
"Guest User",
"guest@example.com",
new Date()
);
}
}
// Uso de los métodos de fábrica
const newUser = User.createNew("Alice", "alice@example.com");
const guestUser = User.createGuest();
// Simulando datos de base de datos
const dbData = {
id: "user_123",
name: "Bob",
email: "bob@example.com",
created_at: "2023-01-15T10:30:00Z"
};
const existingUser = User.fromDatabase(dbData);
En este ejemplo, el constructor es privado, forzando el uso de los métodos de fábrica estáticos para crear instancias.
4. Singleton y estado compartido
Los miembros estáticos permiten implementar el patrón Singleton y mantener estado compartido:
class Logger {
private static instance: Logger | null = null;
private logs: string[] = [];
private constructor() {
// Constructor privado para evitar instanciación directa
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string): void {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
console.log(`[LOG] ${message}`);
}
error(message: string): void {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ERROR: ${message}`);
console.error(`[ERROR] ${message}`);
}
getLogs(): string[] {
return [...this.logs];
}
}
// Uso del singleton
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log("Application started");
logger2.error("Failed to connect to database");
console.log(logger1 === logger2); // true - misma instancia
console.log(logger1.getLogs()); // Muestra todos los logs
5. Contadores y estadísticas
Las propiedades estáticas son útiles para llevar estadísticas globales:
class RequestTracker {
static totalRequests: number = 0;
static successfulRequests: number = 0;
static failedRequests: number = 0;
static trackRequest(success: boolean): void {
RequestTracker.totalRequests++;
if (success) {
RequestTracker.successfulRequests++;
} else {
RequestTracker.failedRequests++;
}
}
static get successRate(): number {
if (this.totalRequests === 0) return 0;
return this.successfulRequests / this.totalRequests;
}
static getStats(): object {
return {
total: this.totalRequests,
successful: this.successfulRequests,
failed: this.failedRequests,
successRate: this.successRate
};
}
}
// Simulando algunas peticiones
RequestTracker.trackRequest(true); // éxito
RequestTracker.trackRequest(true); // éxito
RequestTracker.trackRequest(false); // fallo
RequestTracker.trackRequest(true); // éxito
console.log(RequestTracker.getStats());
// { total: 4, successful: 3, failed: 1, successRate: 0.75 }
Inicialización de propiedades estáticas
Las propiedades estáticas se inicializan cuando se carga la clase, no cuando se crean instancias:
class StaticInitExample {
// Inicialización simple
static counter: number = 0;
// Inicialización con expresión
static readonly CREATED_AT: Date = new Date();
// Inicialización con función
static readonly CONFIG: object = StaticInitExample.loadConfig();
private static loadConfig(): object {
console.log("Loading configuration...");
// Simulando carga de configuración
return {
environment: "development",
debug: true
};
}
constructor() {
StaticInitExample.counter++;
console.log(`Instance created. Total: ${StaticInitExample.counter}`);
}
}
console.log("Before class usage");
console.log(`Creation timestamp: ${StaticInitExample.CREATED_AT}`);
console.log(`Configuration: ${JSON.stringify(StaticInitExample.CONFIG)}`);
// Creando instancias
const instance1 = new StaticInitExample();
const instance2 = new StaticInitExample();
La salida mostrará que la configuración se carga antes de crear cualquier instancia.
Herencia y miembros estáticos
Los miembros estáticos se heredan, pero mantienen su contexto de clase:
class BaseService {
static serviceName: string = "BaseService";
static getServiceInfo(): string {
return `Service: ${this.serviceName}`;
}
static logService(): void {
console.log(`[LOG] ${this.getServiceInfo()}`);
}
}
class UserService extends BaseService {
static override serviceName: string = "UserService";
static override getServiceInfo(): string {
return `${super.getServiceInfo()} (User Management)`;
}
}
BaseService.logService(); // [LOG] Service: BaseService
UserService.logService(); // [LOG] Service: UserService (User Management)
Observa que:
this
en métodos estáticos se refiere a la clase actual- Podemos usar
super
para acceder a implementaciones de la clase base - Las propiedades estáticas se pueden sobrescribir en clases derivadas
Limitaciones y consideraciones
- 1. No pueden acceder a miembros de instancia: Los métodos estáticos no pueden acceder directamente a propiedades o métodos de instancia.
class Example {
instanceValue: number = 42;
static staticMethod(): void {
// Error: no se puede acceder a 'this.instanceValue' desde un contexto estático
// console.log(this.instanceValue);
}
}
- 2. No pueden usar
this
como referencia a instancia: En métodos estáticos,this
se refiere a la clase, no a una instancia.
class StaticThis {
static className: string = "StaticThis";
static printThis(): void {
console.log(this); // Se refiere a la clase StaticThis, no a una instancia
console.log(this.className); // Accede a otra propiedad estática
}
}
StaticThis.printThis();
- 3. Cuidado con la mutabilidad: Las propiedades estáticas mutables son compartidas entre todas las instancias.
class SharedState {
static sharedArray: number[] = [];
addToShared(value: number): void {
SharedState.sharedArray.push(value);
}
}
const obj1 = new SharedState();
const obj2 = new SharedState();
obj1.addToShared(1);
obj2.addToShared(2);
console.log(SharedState.sharedArray); // [1, 2]
Mejores prácticas para usar miembros estáticos
- Usa propiedades estáticas para constantes de clase: Valores que son relevantes para toda la clase.
class HttpStatus {
static readonly OK: number = 200;
static readonly CREATED: number = 201;
static readonly BAD_REQUEST: number = 400;
static readonly NOT_FOUND: number = 404;
static readonly SERVER_ERROR: number = 500;
}
function handleResponse(status: number): void {
if (status === HttpStatus.OK) {
console.log("Request successful");
} else if (status === HttpStatus.NOT_FOUND) {
console.log("Resource not found");
}
}
- Usa métodos estáticos para operaciones independientes del estado: Funcionalidades que no requieren estado de instancia.
class DateUtils {
static formatDate(date: Date, format: string = "yyyy-mm-dd"): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return format
.replace("yyyy", year.toString())
.replace("mm", month)
.replace("dd", day);
}
static getDaysBetween(start: Date, end: Date): number {
const diffTime = Math.abs(end.getTime() - start.getTime());
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
}
const today = new Date();
console.log(DateUtils.formatDate(today)); // "2023-05-15"
- Considera usar clases de utilidad con solo miembros estáticos: Para agrupar funcionalidades relacionadas.
class ValidationUtils {
// Clase con solo métodos estáticos
static isValidEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
static isValidPassword(password: string): boolean {
return password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password);
}
static isValidUsername(username: string): boolean {
return /^[a-zA-Z0-9_]{3,20}$/.test(username);
}
}
// Uso
if (ValidationUtils.isValidEmail("user@example.com")) {
console.log("Email is valid");
}
- Usa métodos estáticos para implementar patrones de diseño: Como Factory, Builder o Singleton.
class Product {
constructor(
public id: string,
public name: string,
public price: number
) {}
// Patrón Builder con métodos estáticos
static builder(): ProductBuilder {
return new ProductBuilder();
}
}
class ProductBuilder {
private id: string = "";
private name: string = "";
private price: number = 0;
setId(id: string): ProductBuilder {
this.id = id;
return this;
}
setName(name: string): ProductBuilder {
this.name = name;
return this;
}
setPrice(price: number): ProductBuilder {
this.price = price;
return this;
}
build(): Product {
return new Product(this.id, this.name, this.price);
}
}
// Uso del patrón Builder
const product = Product.builder()
.setId("prod-123")
.setName("Smartphone")
.setPrice(599.99)
.build();
Las propiedades y métodos estáticos son herramientas poderosas en TypeScript que permiten implementar funcionalidades a nivel de clase, compartir estado entre instancias y crear patrones de diseño elegantes. Cuando se utilizan adecuadamente, pueden mejorar significativamente la organización y reutilización del código.
Aprendizajes de esta lección
- Comprender el concepto de encapsulación y su importancia en la programación orientada a objetos.
- Conocer los modificadores de acceso
public
,private
yprotected
en TypeScript. - Aprender cómo utilizar los modificadores de acceso para controlar la visibilidad de las propiedades y métodos en una clase.
- Entender que las propiedades y métodos
public
son accesibles desde cualquier lugar, mientras que lasprivate
solo desde dentro de la clase y lasprotected
desde la clase y clases heredadas. - Practicar el uso de la encapsulación para ocultar los detalles internos de una clase y evitar accesos no deseados desde el exterior.
- Familiarizarse con los beneficios de la encapsulación, como el mantenimiento del código y la prevención de errores al restringir el acceso a ciertos miembros de la clase.
Completa TypeScript y certifícate
Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs