TypeScript
Tutorial TypeScript: Propiedades y métodos
Aprende a declarar y tipar propiedades y métodos en TypeScript con ejemplos prácticos y buenas prácticas para código seguro y mantenible.
Aprende TypeScript y certifícateDeclaración de propiedades tipadas
Las propiedades son elementos fundamentales en las clases de TypeScript que permiten almacenar datos específicos para cada instancia de objeto. A diferencia de JavaScript, TypeScript nos ofrece la capacidad de tipar estas propiedades, lo que proporciona mayor seguridad y claridad en nuestro código.
Sintaxis básica
Para declarar propiedades tipadas en una clase, debemos especificar el nombre de la propiedad seguido de dos puntos y el tipo de dato que almacenará:
class Product {
name: string;
price: number;
inStock: boolean;
}
En este ejemplo, hemos creado una clase Product
con tres propiedades tipadas: name
como string, price
como number y inStock
como boolean.
Inicialización de propiedades
Existen varias formas de inicializar las propiedades de una clase:
- Inicialización en la declaración:
class Product {
name: string = "Default Product";
price: number = 0;
inStock: boolean = true;
}
- Inicialización en el constructor:
class Product {
name: string;
price: number;
inStock: boolean;
constructor(name: string, price: number, inStock: boolean) {
this.name = name;
this.price = price;
this.inStock = inStock;
}
}
// Uso
const laptop = new Product("Laptop", 999.99, true);
Sintaxis abreviada con parámetros del constructor
TypeScript ofrece una sintaxis abreviada muy útil que combina la declaración de propiedades y su inicialización a través del constructor:
class Product {
constructor(
public name: string,
public price: number,
public inStock: boolean
) {}
}
// Uso
const smartphone = new Product("Smartphone", 599.99, true);
console.log(smartphone.name); // "Smartphone"
Al añadir un modificador de acceso (public
, private
o protected
) a los parámetros del constructor, TypeScript automáticamente:
- Crea la propiedad con el mismo nombre
- Asigna el valor del parámetro a la propiedad
Esta sintaxis reduce significativamente el código repetitivo y es una práctica muy común en proyectos TypeScript.
Tipos complejos para propiedades
Las propiedades pueden utilizar cualquier tipo disponible en TypeScript, desde tipos primitivos hasta tipos más complejos:
class ShoppingCart {
items: Product[] = [];
customer: { id: number; name: string } | null = null;
discountCode?: string;
lastUpdated: Date = new Date();
}
En este ejemplo:
items
es un array de objetosProduct
customer
es un objeto literal onull
discountCode
puede ser unstring
oundefined
(indicado por el operador?
)lastUpdated
es un objeto de tipoDate
Propiedades con tipos personalizados
Para mejorar la legibilidad y reutilización, podemos definir tipos personalizados para nuestras propiedades:
type ProductCategory = "Electronics" | "Clothing" | "Books" | "Food";
type ProductStatus = "Available" | "OutOfStock" | "Discontinued";
class Product {
name: string;
price: number;
category: ProductCategory;
status: ProductStatus;
constructor(name: string, price: number, category: ProductCategory) {
this.name = name;
this.price = price;
this.category = category;
this.status = "Available";
}
}
// Uso
const book = new Product("TypeScript Handbook", 29.99, "Books");
Propiedades estáticas tipadas
Las propiedades estáticas pertenecen a la clase en sí, no a las instancias individuales. También pueden ser tipadas:
class PriceCalculator {
static taxRate: number = 0.21;
static currencySymbol: string = "€";
static calculateTotalPrice(price: number): number {
return price * (1 + this.taxRate);
}
}
// Uso sin instanciar la clase
console.log(PriceCalculator.taxRate); // 0.21
const total = PriceCalculator.calculateTotalPrice(100);
console.log(`${total}${PriceCalculator.currencySymbol}`); // "121€"
Inferencia de tipos en propiedades
TypeScript puede inferir el tipo de una propiedad basándose en su valor inicial:
class Configuration {
// TypeScript infiere que serverUrl es de tipo string
serverUrl = "https://api.example.com";
// TypeScript infiere que maxRetries es de tipo number
maxRetries = 3;
// TypeScript infiere que isProduction es de tipo boolean
isProduction = false;
}
Sin embargo, es una buena práctica declarar explícitamente los tipos, especialmente para propiedades que no se inicializan inmediatamente o que podrían cambiar de valor.
Propiedades con tipos de unión
Podemos definir propiedades que acepten múltiples tipos utilizando tipos de unión:
class UserProfile {
id: string | number;
preferences: string[] | Record<string, boolean>;
constructor(id: string | number) {
this.id = id;
this.preferences = [];
}
updatePreferences(prefs: string[] | Record<string, boolean>): void {
this.preferences = prefs;
}
}
const user1 = new UserProfile("user123");
user1.updatePreferences(["darkMode", "notifications"]);
const user2 = new UserProfile(456);
user2.updatePreferences({ darkMode: true, notifications: false });
En este ejemplo, tanto id
como preferences
pueden aceptar diferentes tipos de datos, lo que proporciona flexibilidad manteniendo la seguridad de tipos.
Buenas prácticas
- Declara siempre el tipo de tus propiedades, incluso cuando TypeScript pueda inferirlo.
- Inicializa las propiedades siempre que sea posible, ya sea en la declaración o en el constructor.
- Utiliza la sintaxis abreviada del constructor para reducir código repetitivo.
- Considera usar tipos personalizados para propiedades con valores específicos o estructuras complejas.
- Mantén la coherencia en el estilo de declaración de propiedades en todo tu proyecto.
Métodos de instancia y su tipado
Los métodos de instancia son funciones que pertenecen a los objetos creados a partir de una clase. En TypeScript, estos métodos pueden ser tipados con precisión, lo que proporciona mayor seguridad y mejor documentación del código. A diferencia de las propiedades que almacenan datos, los métodos definen el comportamiento de los objetos.
Declaración básica de métodos
Para declarar un método en una clase de TypeScript, utilizamos una sintaxis similar a la de las funciones regulares, pero dentro del cuerpo de la clase:
class Product {
name: string;
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
displayInfo(): void {
console.log(`Product: ${this.name}, Price: $${this.price}`);
}
applyDiscount(percentage: number): number {
return this.price * (1 - percentage / 100);
}
}
const laptop = new Product("Gaming Laptop", 1299.99);
laptop.displayInfo(); // "Product: Gaming Laptop, Price: $1299.99"
const discountedPrice = laptop.applyDiscount(10);
console.log(`Discounted price: $${discountedPrice}`); // "Discounted price: $1169.99"
En este ejemplo:
displayInfo()
es un método que no devuelve ningún valor (tipovoid
)applyDiscount()
recibe un parámetro numérico y devuelve otro número (tiponumber
)
Tipado de parámetros y valores de retorno
La principal ventaja de TypeScript es poder especificar los tipos de los parámetros y valores de retorno de los métodos:
class ShoppingCart {
items: Product[] = [];
addItem(product: Product): void {
this.items.push(product);
}
removeItem(productName: string): boolean {
const initialLength = this.items.length;
this.items = this.items.filter(item => item.name !== productName);
return this.items.length !== initialLength;
}
calculateTotal(): number {
return this.items.reduce((total, item) => total + item.price, 0);
}
getItemCount(): number {
return this.items.length;
}
}
Cada método tiene claramente definido:
- Los tipos de sus parámetros (como
product: Product
oproductName: string
) - El tipo de retorno (
void
,boolean
,number
)
Métodos con parámetros opcionales y predeterminados
Al igual que con las funciones regulares, los métodos pueden tener parámetros opcionales o valores predeterminados:
class OrderProcessor {
processOrder(orderId: string, quantity: number, express?: boolean): void {
// El parámetro express es opcional
const shippingMethod = express ? "Express" : "Standard";
console.log(`Processing order ${orderId} for ${quantity} items with ${shippingMethod} shipping`);
}
calculateShipping(distance: number, weight: number, priority: boolean = false): number {
// El parámetro priority tiene un valor predeterminado de false
const baseRate = distance * 0.1;
const weightFactor = weight * 0.5;
const priorityMultiplier = priority ? 1.5 : 1;
return (baseRate + weightFactor) * priorityMultiplier;
}
}
const processor = new OrderProcessor();
processor.processOrder("ORD-123", 3); // Sin el parámetro opcional
processor.processOrder("ORD-456", 1, true); // Con el parámetro opcional
const standardShipping = processor.calculateShipping(100, 5); // Usa el valor predeterminado
const priorityShipping = processor.calculateShipping(100, 5, true); // Sobrescribe el valor predeterminado
Sobrecarga de métodos
TypeScript permite la sobrecarga de métodos, lo que significa definir múltiples firmas para un mismo método:
class Calculator {
// Sobrecargas
add(a: number, b: number): number;
add(a: string, b: string): string;
// Implementación
add(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a + b;
}
if (typeof a === "string" && typeof b === "string") {
return a.concat(b);
}
throw new Error("Parameters must be both numbers or both strings");
}
}
const calc = new Calculator();
const sum = calc.add(5, 3); // 8
const text = calc.add("Hello, ", "World!"); // "Hello, World!"
Las sobrecargas proporcionan una interfaz más clara para los usuarios de la clase, ya que TypeScript mostrará las firmas específicas según los argumentos proporcionados.
Métodos asíncronos
Los métodos pueden ser asíncronos utilizando async/await
y devolver promesas tipadas:
class DataService {
async fetchUserData(userId: string): Promise<UserData> {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user data: ${response.statusText}`);
}
return await response.json() as UserData;
}
async saveUserPreferences(userId: string, preferences: UserPreferences): Promise<boolean> {
try {
const response = await fetch(`https://api.example.com/users/${userId}/preferences`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(preferences)
});
return response.ok;
} catch (error) {
console.error("Failed to save preferences:", error);
return false;
}
}
}
// Tipos utilizados
interface UserData {
id: string;
name: string;
email: string;
}
interface UserPreferences {
theme: "light" | "dark";
notifications: boolean;
}
En este ejemplo:
fetchUserData()
devuelve unaPromise<UserData>
saveUserPreferences()
devuelve unaPromise<boolean>
Métodos con tipos genéricos
Los métodos pueden utilizar tipos genéricos para crear funciones más flexibles pero tipadas:
class Repository<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getById<K extends keyof T>(property: K, value: T[K]): T | undefined {
return this.items.find(item => item[property] === value);
}
filter(predicate: (item: T) => boolean): T[] {
return this.items.filter(predicate);
}
map<R>(transformer: (item: T) => R): R[] {
return this.items.map(transformer);
}
}
// Uso con un tipo específico
interface Product {
id: number;
name: string;
price: number;
}
const productRepo = new Repository<Product>();
productRepo.add({ id: 1, name: "Laptop", price: 999 });
productRepo.add({ id: 2, name: "Phone", price: 699 });
const laptop = productRepo.getById("id", 1);
const expensiveProducts = productRepo.filter(p => p.price > 800);
const productNames = productRepo.map(p => p.name); // string[]
Métodos con tipos de unión y guardas de tipo
Los métodos pueden trabajar con tipos de unión y utilizar guardas de tipo para manejar diferentes casos:
type PaymentMethod = CreditCard | BankTransfer | PayPal;
interface CreditCard {
type: "credit";
cardNumber: string;
expiryDate: string;
}
interface BankTransfer {
type: "bank";
accountNumber: string;
bankCode: string;
}
interface PayPal {
type: "paypal";
email: string;
}
class PaymentProcessor {
processPayment(amount: number, method: PaymentMethod): boolean {
console.log(`Processing payment of $${amount}`);
// Guarda de tipo usando la propiedad discriminante "type"
if (method.type === "credit") {
console.log(`Using credit card ending in ${method.cardNumber.slice(-4)}`);
// Lógica específica para tarjetas de crédito
} else if (method.type === "bank") {
console.log(`Using bank account ${method.accountNumber}`);
// Lógica específica para transferencias bancarias
} else {
console.log(`Using PayPal account ${method.email}`);
// Lógica específica para PayPal
}
return true;
}
}
Encadenamiento de métodos
Podemos implementar el patrón de encadenamiento (method chaining) haciendo que los métodos devuelvan this
:
class QueryBuilder {
private filters: Record<string, any> = {};
private sortField: string | null = null;
private sortDirection: "asc" | "desc" = "asc";
private limitValue: number | null = null;
where(field: string, value: any): this {
this.filters[field] = value;
return this;
}
orderBy(field: string, direction: "asc" | "desc" = "asc"): this {
this.sortField = field;
this.sortDirection = direction;
return this;
}
limit(value: number): this {
this.limitValue = value;
return this;
}
execute(): object[] {
console.log("Executing query with:");
console.log("Filters:", this.filters);
console.log("Sort:", this.sortField, this.sortDirection);
console.log("Limit:", this.limitValue);
// Aquí iría la lógica real de consulta
return [];
}
}
// Uso con encadenamiento
const results = new QueryBuilder()
.where("category", "electronics")
.where("price", { $lt: 1000 })
.orderBy("rating", "desc")
.limit(10)
.execute();
Buenas prácticas para métodos de instancia
- Nombres descriptivos: Usa verbos que describan claramente la acción que realiza el método.
- Responsabilidad única: Cada método debe hacer una sola cosa y hacerla bien.
- Parámetros tipados: Siempre especifica los tipos de los parámetros.
- Valores de retorno: Declara explícitamente el tipo de retorno, incluso cuando sea
void
. - Métodos pequeños: Mantén los métodos concisos y enfocados en una tarea específica.
- Documentación: Considera usar comentarios JSDoc para documentar métodos complejos:
class DataAnalyzer {
/**
* Calcula estadísticas sobre un conjunto de datos numéricos
* @param data - Array de números para analizar
* @returns Objeto con estadísticas calculadas
*/
calculateStats(data: number[]): { min: number; max: number; avg: number } {
if (data.length === 0) {
throw new Error("Cannot calculate stats for empty dataset");
}
const min = Math.min(...data);
const max = Math.max(...data);
const sum = data.reduce((acc, val) => acc + val, 0);
const avg = sum / data.length;
return { min, max, avg };
}
}
Propiedades opcionales
Las propiedades opcionales en TypeScript permiten definir atributos que pueden existir o no en una clase, lo que aporta flexibilidad al diseño de nuestros objetos. Esta característica es especialmente útil cuando trabajamos con estructuras de datos donde ciertos campos no siempre están presentes o cuando implementamos patrones como el constructor con opciones.
Sintaxis básica
Para declarar una propiedad como opcional, simplemente añadimos el símbolo de interrogación (?
) después del nombre de la propiedad:
class User {
id: number;
name: string;
email: string;
phoneNumber?: string;
address?: string;
}
En este ejemplo, phoneNumber
y address
son propiedades opcionales, mientras que id
, name
y email
son obligatorias.
Inicialización y acceso seguro
Al trabajar con propiedades opcionales, debemos tener en cuenta que pueden ser undefined
:
class User {
id: number;
name: string;
email: string;
phoneNumber?: string;
constructor(id: number, name: string, email: string, phoneNumber?: string) {
this.id = id;
this.name = name;
this.email = email;
this.phoneNumber = phoneNumber;
}
displayContact(): string {
// Uso del operador de encadenamiento opcional
return `Contact: ${this.email} ${this.phoneNumber?.length ? `/ ${this.phoneNumber}` : ''}`;
}
}
const user1 = new User(1, "Alice", "alice@example.com");
const user2 = new User(2, "Bob", "bob@example.com", "555-1234");
console.log(user1.displayContact()); // "Contact: alice@example.com"
console.log(user2.displayContact()); // "Contact: bob@example.com / 555-1234"
El operador de encadenamiento opcional (?.
) nos permite acceder de forma segura a propiedades que podrían ser undefined
, evitando errores en tiempo de ejecución.
Patrón de opciones con propiedades opcionales
Un uso común de las propiedades opcionales es implementar el patrón de opciones, donde pasamos un objeto de configuración al constructor:
interface ProductOptions {
description?: string;
category?: string;
tags?: string[];
discount?: number;
featured?: boolean;
}
class Product {
id: number;
name: string;
price: number;
description?: string;
category?: string;
tags?: string[];
discount?: number;
featured: boolean = false;
constructor(id: number, name: string, price: number, options?: ProductOptions) {
this.id = id;
this.name = name;
this.price = price;
// Asignamos las opciones si están presentes
if (options) {
this.description = options.description;
this.category = options.category;
this.tags = options.tags;
this.discount = options.discount;
this.featured = options.featured ?? false;
}
}
getDisplayPrice(): number {
// Aplicamos descuento si existe
return this.discount ? this.price * (1 - this.discount) : this.price;
}
}
// Uso básico sin opciones
const basicProduct = new Product(1, "Basic Widget", 19.99);
// Uso con algunas opciones
const featuredProduct = new Product(2, "Premium Widget", 49.99, {
description: "Our best-selling widget with premium features",
category: "Premium",
featured: true,
discount: 0.1 // 10% de descuento
});
console.log(basicProduct.getDisplayPrice()); // 19.99
console.log(featuredProduct.getDisplayPrice()); // 44.991 (con 10% de descuento)
Este patrón hace que nuestro código sea más legible y mantenible, especialmente cuando tenemos muchos parámetros opcionales.
Propiedades opcionales con tipos de unión
Podemos combinar propiedades opcionales con tipos de unión para mayor flexibilidad:
class Notification {
id: string;
title: string;
message: string;
timestamp: Date;
// Puede ser un string, un objeto User, o no existir
recipient?: string | { id: number; email: string };
// Puede ser "info", "warning", "error" o no existir (undefined)
type?: "info" | "warning" | "error";
constructor(id: string, title: string, message: string) {
this.id = id;
this.title = title;
this.message = message;
this.timestamp = new Date();
}
send(): boolean {
console.log(`Sending notification: ${this.title}`);
if (this.recipient) {
if (typeof this.recipient === "string") {
console.log(`To email: ${this.recipient}`);
} else {
console.log(`To user: ${this.recipient.email}`);
}
} else {
console.log("Broadcasting to all users");
}
return true;
}
}
// Notificación general sin destinatario específico
const broadcastNotification = new Notification(
"not-1",
"System Maintenance",
"The system will be down for maintenance tonight"
);
// Notificación con destinatario específico
const userNotification = new Notification(
"not-2",
"Welcome!",
"Thank you for joining our platform"
);
userNotification.recipient = { id: 42, email: "new.user@example.com" };
userNotification.type = "info";
broadcastNotification.send();
userNotification.send();
Propiedades opcionales vs. inicialización con valores por defecto
Es importante entender la diferencia entre una propiedad opcional y una propiedad con valor por defecto:
class Configuration {
// Propiedad opcional: puede ser string o undefined
apiKey?: string;
// Propiedad con valor por defecto: siempre será string
theme: string = "light";
// Propiedad opcional con valor por defecto si se proporciona
maxRetries?: number;
constructor(apiKey?: string, theme?: string, maxRetries?: number) {
if (apiKey) this.apiKey = apiKey;
if (theme) this.theme = theme;
this.maxRetries = maxRetries;
}
isValid(): boolean {
// Una configuración es válida solo si tiene apiKey
return !!this.apiKey;
}
}
const config1 = new Configuration(); // apiKey: undefined, theme: "light", maxRetries: undefined
const config2 = new Configuration("abc123"); // apiKey: "abc123", theme: "light", maxRetries: undefined
const config3 = new Configuration("xyz789", "dark", 3); // apiKey: "xyz789", theme: "dark", maxRetries: 3
console.log(config1.isValid()); // false
console.log(config2.isValid()); // true
- Una propiedad opcional (
apiKey?
) puede no existir en el objeto. - Una propiedad con valor por defecto (
theme = "light"
) siempre existirá, pero tendrá un valor predeterminado si no se especifica otro.
Propiedades opcionales en interfaces implementadas
Cuando una clase implementa una interfaz con propiedades opcionales, debe respetar esa opcionalidad:
interface Vehicle {
make: string;
model: string;
year: number;
color?: string;
features?: string[];
}
class Car implements Vehicle {
make: string;
model: string;
year: number;
color?: string;
features?: string[];
// Propiedad adicional específica de Car
fuelType: string;
constructor(make: string, model: string, year: number, fuelType: string) {
this.make = make;
this.model = model;
this.year = year;
this.fuelType = fuelType;
}
addFeature(feature: string): void {
// Inicializamos el array si no existe
if (!this.features) {
this.features = [];
}
this.features.push(feature);
}
}
const myCar = new Car("Toyota", "Corolla", 2022, "Hybrid");
myCar.color = "Blue";
myCar.addFeature("Navigation");
myCar.addFeature("Bluetooth");
console.log(myCar);
Operador de coalescencia nula con propiedades opcionales
El operador de coalescencia nula (??
) es muy útil al trabajar con propiedades opcionales, ya que nos permite proporcionar un valor predeterminado cuando la propiedad es undefined
:
class ShoppingCart {
items: { product: string; price: number }[] = [];
customer?: { name: string; email: string };
discountCode?: string;
calculateTotal(): number {
const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
const discount = this.getDiscountPercentage();
return subtotal * (1 - discount);
}
getDiscountPercentage(): number {
// Si hay un código de descuento, aplicamos 10%, de lo contrario 0%
return this.discountCode ? 0.1 : 0;
}
getCustomerName(): string {
// Usamos el operador ?? para proporcionar un valor predeterminado
return this.customer?.name ?? "Guest";
}
checkout(): boolean {
const total = this.calculateTotal();
console.log(`Checkout for ${this.getCustomerName()}`);
console.log(`Total: $${total.toFixed(2)}`);
// Verificamos si tenemos información del cliente
if (this.customer) {
console.log(`Sending receipt to ${this.customer.email}`);
} else {
console.log("No customer information provided");
}
return true;
}
}
const cart1 = new ShoppingCart();
cart1.items.push({ product: "Keyboard", price: 59.99 });
cart1.items.push({ product: "Mouse", price: 29.99 });
const cart2 = new ShoppingCart();
cart2.items.push({ product: "Monitor", price: 299.99 });
cart2.customer = { name: "Alice", email: "alice@example.com" };
cart2.discountCode = "SUMMER10";
cart1.checkout(); // Checkout for Guest, sin descuento
cart2.checkout(); // Checkout for Alice, con 10% de descuento
Buenas prácticas con propiedades opcionales
- Usa propiedades opcionales cuando un campo genuinamente puede no existir.
- Considera valores por defecto en lugar de propiedades opcionales cuando casi siempre debería haber un valor.
- Verifica la existencia de propiedades opcionales antes de usarlas (con
?.
,??
o comprobaciones explícitas). - Documenta el propósito de las propiedades opcionales para que otros desarrolladores entiendan cuándo pueden estar ausentes.
- Inicializa colecciones vacías en lugar de hacerlas opcionales cuando sea apropiado (por ejemplo,
tags: string[] = []
en lugar detags?: string[]
). - Combina con destructuración para un código más limpio al trabajar con objetos de opciones:
interface UserOptions {
role?: string;
isActive?: boolean;
preferences?: {
language?: string;
notifications?: boolean;
};
}
class UserManager {
createUser(name: string, email: string, options?: UserOptions): void {
// Destructuración con valores por defecto
const {
role = "user",
isActive = true,
preferences = {}
} = options || {};
const {
language = "en",
notifications = true
} = preferences;
console.log(`Creating user: ${name} (${email})`);
console.log(`Role: ${role}, Active: ${isActive}`);
console.log(`Language: ${language}, Notifications: ${notifications}`);
// Lógica para crear el usuario...
}
}
const userManager = new UserManager();
// Diferentes niveles de opcionalidad
userManager.createUser("John", "john@example.com");
userManager.createUser("Sarah", "sarah@example.com", { role: "admin" });
userManager.createUser("Mike", "mike@example.com", {
isActive: false,
preferences: { language: "es" }
});
Propiedades de solo lectura (readonly)
Las propiedades de solo lectura en TypeScript proporcionan un mecanismo de inmutabilidad que permite definir atributos que pueden asignarse durante la inicialización pero que no pueden modificarse posteriormente. Esta característica refuerza la integridad de los datos y ayuda a prevenir cambios accidentales en propiedades que deberían permanecer constantes.
Sintaxis básica
Para declarar una propiedad como de solo lectura, utilizamos la palabra clave readonly
antes del nombre de la propiedad:
class Product {
readonly id: string;
name: string;
price: number;
constructor(id: string, name: string, price: number) {
this.id = id;
this.name = name;
this.price = price;
}
}
const laptop = new Product("P001", "Gaming Laptop", 1299.99);
console.log(laptop.id); // "P001"
// Error: Cannot assign to 'id' because it is a read-only property
// laptop.id = "P002";
En este ejemplo, la propiedad id
solo puede establecerse en el constructor o en su declaración inicial, pero no puede modificarse después.
Inicialización de propiedades readonly
Las propiedades de solo lectura pueden inicializarse de varias maneras:
- En la declaración:
class Configuration {
readonly apiVersion: string = "v1";
readonly maxConnections: number = 100;
readonly createdAt: Date = new Date();
// Otras propiedades y métodos...
}
- En el constructor:
class User {
readonly id: string;
readonly registrationDate: Date;
name: string;
constructor(id: string, name: string) {
this.id = id;
this.registrationDate = new Date();
this.name = name;
}
}
- Con la sintaxis abreviada del constructor:
class Transaction {
constructor(
readonly id: string,
readonly timestamp: Date,
readonly amount: number,
public status: "pending" | "completed" | "failed" = "pending"
) {}
completeTransaction(): void {
this.status = "completed";
// No podemos modificar las propiedades readonly:
// this.amount = 0; // Error
}
}
const payment = new Transaction("T123", new Date(), 99.99);
Propiedades readonly vs const
Es importante entender la diferencia entre propiedades readonly
y variables const
:
class Example {
readonly readonlyProperty: number = 42;
static readonly STATIC_CONSTANT: string = "FIXED_VALUE";
demonstrateDifference(): void {
// Variables const
const localConstant = 100;
// localConstant = 200; // Error: no se puede reasignar
// Pero si localConstant es un objeto, sus propiedades pueden modificarse
const user = { name: "Alice" };
user.name = "Bob"; // Esto es válido
// Con readonly, la propiedad en sí no puede modificarse
// this.readonlyProperty = 43; // Error
console.log(Example.STATIC_CONSTANT); // Acceso a constante estática
}
}
readonly
se aplica a propiedades de clases e interfacesconst
se aplica a variables y no puede reasignarse- Un objeto asignado a una variable
const
puede tener sus propiedades modificadas - Una propiedad
readonly
no puede modificarse, independientemente de dónde se use
Propiedades readonly con tipos complejos
Cuando una propiedad readonly
contiene un objeto o array, la referencia es inmutable, pero el contenido puede modificarse:
class Department {
readonly name: string;
readonly code: string;
readonly employees: string[];
constructor(name: string, code: string, initialEmployees: string[] = []) {
this.name = name;
this.code = code;
this.employees = initialEmployees;
}
addEmployee(name: string): void {
// Podemos modificar el contenido del array readonly
this.employees.push(name);
// Pero no podemos reasignar el array
// this.employees = []; // Error
}
}
const engineering = new Department("Engineering", "ENG", ["Alice"]);
engineering.addEmployee("Bob");
console.log(engineering.employees); // ["Alice", "Bob"]
Para lograr una inmutabilidad completa, podemos usar técnicas adicionales:
class ImmutableCollection {
readonly data: ReadonlyArray<number>;
constructor(initialData: number[]) {
// Asignamos un ReadonlyArray
this.data = initialData;
}
// Para "modificar", creamos una nueva instancia
addItem(item: number): ImmutableCollection {
const newData = [...this.data, item];
return new ImmutableCollection(newData);
}
getItems(): ReadonlyArray<number> {
return this.data;
}
}
let collection = new ImmutableCollection([1, 2, 3]);
// collection.data.push(4); // Error: push no existe en ReadonlyArray
// collection.data[0] = 99; // Error: no se puede asignar a un índice
// En su lugar, creamos una nueva colección
collection = collection.addItem(4);
console.log(collection.data); // [1, 2, 3, 4]
Propiedades readonly en interfaces
Las propiedades de solo lectura también pueden definirse en interfaces:
interface Point {
readonly x: number;
readonly y: number;
}
class Circle {
readonly center: Point;
radius: number;
constructor(x: number, y: number, radius: number) {
this.center = { x, y };
this.radius = radius;
}
move(newX: number, newY: number): Circle {
// No podemos modificar center directamente
// this.center.x = newX; // Error
// Creamos un nuevo círculo
return new Circle(newX, newY, this.radius);
}
resize(newRadius: number): void {
// El radio no es readonly, así que podemos modificarlo
this.radius = newRadius;
}
}
let circle = new Circle(0, 0, 5);
circle = circle.move(10, 10);
Propiedades readonly estáticas
Las propiedades estáticas también pueden ser de solo lectura, lo que es útil para definir constantes a nivel de clase:
class AppSettings {
// Constantes de configuración
static readonly API_URL: string = "https://api.example.com";
static readonly MAX_RETRY_ATTEMPTS: number = 3;
static readonly DEFAULT_TIMEOUT: number = 5000;
// Valores calculados que no deben cambiar
static readonly APP_VERSION: string = AppSettings.calculateVersion();
private static calculateVersion(): string {
// Lógica para determinar la versión
return "1.2.3";
}
static getApiEndpoint(resource: string): string {
return `${AppSettings.API_URL}/${resource}`;
}
}
// Uso
console.log(AppSettings.API_URL);
const usersEndpoint = AppSettings.getApiEndpoint("users");
// Error: Cannot assign to 'API_URL' because it is a read-only property
// AppSettings.API_URL = "https://new-api.example.com";
Readonly en mapeos de tipos
TypeScript proporciona el tipo de utilidad Readonly<T>
que convierte todas las propiedades de un tipo en solo lectura:
interface UserData {
id: string;
name: string;
email: string;
preferences: {
theme: string;
notifications: boolean;
};
}
class UserProfile {
readonly data: Readonly<UserData>;
constructor(userData: UserData) {
this.data = userData;
}
updatePreferences(theme?: string, notifications?: boolean): UserProfile {
// Creamos un nuevo objeto con los cambios
const newData: UserData = {
...this.data,
preferences: {
...this.data.preferences,
...(theme !== undefined && { theme }),
...(notifications !== undefined && { notifications })
}
};
return new UserProfile(newData);
}
}
let profile = new UserProfile({
id: "user123",
name: "John Doe",
email: "john@example.com",
preferences: {
theme: "light",
notifications: true
}
});
// No podemos modificar directamente
// profile.data.name = "Jane"; // Error
// profile.data.preferences.theme = "dark"; // Error
// En su lugar, creamos un nuevo perfil con los cambios
profile = profile.updatePreferences("dark");
console.log(profile.data.preferences.theme); // "dark"
Readonly en genéricos
Podemos crear clases genéricas que aprovechen las propiedades de solo lectura:
class ReadonlyRepository<T extends { id: string }> {
private items: Map<string, Readonly<T>>;
constructor(initialItems: T[] = []) {
this.items = new Map();
initialItems.forEach(item => this.items.set(item.id, Object.freeze({...item})));
}
getById(id: string): Readonly<T> | undefined {
return this.items.get(id);
}
getAll(): ReadonlyArray<Readonly<T>> {
return Array.from(this.items.values());
}
// Para "modificar", creamos nuevas versiones
update(id: string, updates: Partial<Omit<T, "id">>): boolean {
const existing = this.items.get(id);
if (!existing) return false;
const updated = { ...existing, ...updates };
this.items.set(id, Object.freeze(updated));
return true;
}
}
interface Product {
id: string;
name: string;
price: number;
}
const productRepo = new ReadonlyRepository<Product>([
{ id: "p1", name: "Laptop", price: 999 }
]);
const laptop = productRepo.getById("p1");
// laptop.price = 899; // Error: no se puede modificar
// En su lugar, usamos el método update
productRepo.update("p1", { price: 899 });
Buenas prácticas para propiedades readonly
- Usa readonly para identificadores: Los IDs, códigos y otros identificadores únicos son candidatos ideales para propiedades readonly.
- Considera readonly para fechas de creación: Timestamps como fechas de creación normalmente no deberían cambiar.
- Aplica readonly a configuraciones inmutables: Valores de configuración que no deben cambiar durante la vida del objeto.
- Combina readonly con métodos inmutables: Para objetos complejos, implementa métodos que devuelvan nuevas instancias en lugar de modificar el estado.
- Usa ReadonlyArray y ReadonlyMap: Para colecciones que no deben modificarse.
- Considera Object.freeze: Para una inmutabilidad más profunda, puedes combinar readonly con
Object.freeze()
.
class ConfigurationManager {
readonly settings: Readonly<Record<string, any>>;
constructor(initialSettings: Record<string, any>) {
// Congelamos el objeto para prevenir modificaciones profundas
this.settings = Object.freeze({...initialSettings});
}
// Para "modificar", creamos una nueva instancia
withSetting(key: string, value: any): ConfigurationManager {
return new ConfigurationManager({
...this.settings,
[key]: value
});
}
}
let config = new ConfigurationManager({
apiUrl: "https://api.example.com",
timeout: 5000,
retryAttempts: 3
});
// No podemos modificar directamente
// config.settings.timeout = 10000; // Error
// En su lugar, creamos una nueva configuración
config = config.withSetting("timeout", 10000);
console.log(config.settings.timeout); // 10000
Las propiedades de solo lectura son una herramienta poderosa para diseñar APIs más seguras y predecibles, especialmente cuando se combinan con patrones de programación inmutables que favorecen la creación de nuevos objetos sobre la modificación de los existentes.
Ejercicios de esta lección Propiedades y métodos
Evalúa tus conocimientos de esta lección Propiedades y métodos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Funciones
Reto composición de funciones
Reto tipos especiales
Reto tipos genéricos
Módulos
Polimorfismo
Funciones TypeScript
Interfaces
Funciones puras
Reto namespaces
Funciones flecha
Polimorfismo
Operadores
Conversor de unidades
Funciones flecha
Control de flujo
Herencia
Clases
Proyecto validación de tipado
Clases y objetos
Encapsulación
Herencia
Proyecto sistema de votación
Reto genéricos con clases
Inmutabilidad
Interfaces
Funciones de alto orden
Reto map y filter
Control de flujo
Interfaces
Reto funciones orden superior
Herencia y clases abstractas
Reto tipos mapped
Herencia de clases
Reto funciones puras
Variables y constantes
Introducción a TypeScript
Reto testing unitario
Funciones de primera clase
Clases
OOP y CRUD en TypeScript
Interfaces y su implementación
Tipos genéricos
Namespaces
Proyecto calculadora gastos
Operadores y expresiones
Proyecto generador de contraseñas
Reto unión e intersección
Encapsulación
Tipos de unión e intersección
Tipos de unión e intersección
Reto hola mundo en TS
Variables y constantes
Funciones puras
Control de flujo
Introducción a TypeScript
Resolución de módulos
Control de flujo
Reto tipos de utilidad
Reto tipos literales y condicionales
Reto exportar e importar
Propiedades y métodos
Tipos de utilidad
Clases y objetos
Tipos de datos, variables y constantes
Proyecto Minigestor de tareas
Operadores
Funciones flecha y contexto
Funciones
Reto type aliases
Funciones de alto orden
Funciones y parámetros tipados
Tipos literales
Reto enums
Tipos de utilidad
Modificadores de acceso y encapsulación
Polimorfismo
Tipos genéricos
Reto módulos
Tipos literales
Inmutabilidad
Proyecto Generator de datos
Variables y constantes
Funciones de primera clase
Todas las lecciones de TypeScript
Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Typescript
Introducción Y Entorno
Instalación Y Configuración De Typescript
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Funciones Y Parámetros Tipados
Sintaxis
Funciones Flecha Y Contexto
Sintaxis
Enums
Sintaxis
Type Aliases Y Aserciones De Tipo
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Interfaces Y Su Implementación
Programación Orientada A Objetos
Modificadores De Acceso Y Encapsulación
Programación Orientada A Objetos
Herencia Y Clases Abstractas
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Decoradores Básicos
Programación Orientada A Objetos
Propiedades Y Métodos
Programación Orientada A Objetos
Inmutabilidad
Programación Funcional
Funciones Puras
Programación Funcional
Funciones De Primera Clase
Programación Funcional
Funciones De Alto Orden
Programación Funcional
Conceptos Básicos E Inmutabilidad
Programación Funcional
Funciones De Primera Clase Y Orden Superior
Programación Funcional
Composición De Funciones
Programación Funcional
Métodos Funcionales De Arrays (Map, Filter, Reduce)
Programación Funcional
Tipos Literales
Tipos Intermedios Y Avanzados
Tipos Genéricos
Tipos Intermedios Y Avanzados
Tipos De Unión E Intersección
Tipos Intermedios Y Avanzados
Tipos De Utilidad
Tipos Intermedios Y Avanzados
Unknown, Never Y Tipos Especiales
Tipos Intermedios Y Avanzados
Tipos Mapped
Tipos Intermedios Y Avanzados
Genéricos Con Clases E Interfaces
Tipos Intermedios Y Avanzados
Módulos
Namespaces Y Módulos
Namespaces
Namespaces Y Módulos
Resolución De Módulos
Namespaces Y Módulos
Exportación E Importación De Módulos
Namespaces Y Módulos
Introducción A Módulos
Namespaces Y Módulos
Testing Unitario En Typescript
Testing
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la sintaxis básica para declarar propiedades tipadas en clases de TypeScript
- Dominar las diferentes formas de inicializar propiedades, incluyendo la sintaxis abreviada del constructor
- Implementar propiedades opcionales para crear estructuras flexibles usando el operador de interrogación (?)
- Utilizar propiedades de solo lectura (readonly) para garantizar la inmutabilidad de ciertos datos
- Aplicar buenas prácticas en la declaración y uso de propiedades para mejorar la seguridad y legibilidad del código