Español

Desbloquea el poder de las estructuras de datos inmutables en TypeScript con los tipos readonly. Aprende a crear aplicaciones más predecibles, mantenibles y robustas evitando mutaciones de datos no deseadas.

Tipos Readonly de TypeScript: Dominando las Estructuras de Datos Inmutables

En el panorama siempre cambiante del desarrollo de software, la búsqueda de código robusto, predecible y mantenible es un esfuerzo constante. TypeScript, con su sistema de tipado fuerte, proporciona herramientas poderosas para alcanzar estos objetivos. Entre estas herramientas, los tipos readonly se destacan como un mecanismo crucial para hacer cumplir la inmutabilidad, una piedra angular de la programación funcional y una clave para construir aplicaciones más fiables.

¿Qué es la Inmutabilidad y por qué es Importante?

La inmutabilidad, en esencia, significa que una vez que un objeto es creado, su estado no puede ser cambiado. Este simple concepto tiene profundas implicaciones para la calidad y mantenibilidad del código.

Tipos Readonly en TypeScript: Tu Arsenal de Inmutabilidad

TypeScript proporciona varias formas de hacer cumplir la inmutabilidad usando la palabra clave readonly. Exploremos las diferentes técnicas y cómo se pueden aplicar en la práctica.

1. Propiedades Readonly en Interfaces y Tipos

La forma más directa de declarar una propiedad como readonly es usar la palabra clave readonly directamente en una definición de interfaz o tipo.


interface Person {
  readonly id: string;
  name: string;
  age: number;
}

const person: Person = {
  id: "unique-id-123",
  name: "Alice",
  age: 30,
};

// person.id = "new-id"; // Error: No se puede asignar a 'id' porque es una propiedad de solo lectura.
person.name = "Bob"; // Esto está permitido

En este ejemplo, la propiedad id se declara como readonly. TypeScript evitará cualquier intento de modificarla después de que el objeto sea creado. Las propiedades name y age, al carecer del modificador readonly, pueden modificarse libremente.

2. El Tipo de Utilidad Readonly

TypeScript ofrece un potente tipo de utilidad llamado Readonly<T>. Este tipo genérico toma un tipo existente T y lo transforma haciendo que todas sus propiedades sean readonly.


interface Point {
  x: number;
  y: number;
}

const point: Readonly<Point> = {
  x: 10,
  y: 20,
};

// point.x = 30; // Error: No se puede asignar a 'x' porque es una propiedad de solo lectura.

El tipo Readonly<Point> crea un nuevo tipo donde tanto x como y son readonly. Esta es una forma conveniente de hacer rápidamente inmutable un tipo existente.

3. Arrays Readonly (ReadonlyArray<T>) y readonly T[]

Los arrays en JavaScript son inherentemente mutables. TypeScript proporciona una forma de crear arrays readonly usando el tipo ReadonlyArray<T> o la abreviatura readonly T[]. Esto previene la modificación del contenido del array.


const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// numbers.push(6); // Error: La propiedad 'push' no existe en el tipo 'readonly number[]'.
// numbers[0] = 10; // Error: La firma de índice en el tipo 'readonly number[]' solo permite la lectura.

const moreNumbers: readonly number[] = [6, 7, 8, 9, 10]; // Equivalente a ReadonlyArray
// moreNumbers.push(11); // Error: La propiedad 'push' no existe en el tipo 'readonly number[]'.

Intentar usar métodos que modifican el array, como push, pop, splice, o asignar directamente a un índice, resultará en un error de TypeScript.

4. const vs. readonly: Entendiendo la Diferencia

Es importante distinguir entre const y readonly. const previene la reasignación de la variable misma, mientras que readonly previene la modificación de las propiedades del objeto. Sirven para propósitos diferentes y pueden usarse juntos para una máxima inmutabilidad.


const immutableNumber = 42;
// immutableNumber = 43; // Error: No se puede reasignar a la variable const 'immutableNumber'.

const mutableObject = { value: 10 };
mutableObject.value = 20; // Esto está permitido porque el *objeto* no es const, solo la variable.

const readonlyObject: Readonly<{ value: number }> = { value: 30 };
// readonlyObject.value = 40; // Error: No se puede asignar a 'value' porque es una propiedad de solo lectura.

const constReadonlyObject: Readonly<{ value: number }> = { value: 50 };
// constReadonlyObject = { value: 60 }; // Error: No se puede reasignar a la variable const 'constReadonlyObject'.
// constReadonlyObject.value = 60; // Error: No se puede asignar a 'value' porque es una propiedad de solo lectura.

Como se demostró anteriormente, const asegura que la variable siempre apunte al mismo objeto en memoria, mientras que readonly garantiza que el estado interno del objeto permanezca sin cambios.

Ejemplos Prácticos: Aplicando Tipos Readonly en Escenarios del Mundo Real

Exploremos algunos ejemplos prácticos de cómo los tipos readonly pueden ser usados para mejorar la calidad y mantenibilidad del código en varios escenarios.

1. Gestionando Datos de Configuración

Los datos de configuración a menudo se cargan una vez al inicio de la aplicación y no deben ser modificados durante la ejecución. Usar tipos readonly asegura que estos datos permanezcan consistentes y previene modificaciones accidentales.


interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
  readonly features: readonly string[];
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  features: ["featureA", "featureB"],
};

function fetchData(url: string, config: Readonly<AppConfig>) {
    // ... usa config.timeout y config.apiUrl de forma segura, sabiendo que no cambiarán
}

fetchData("/data", config);

2. Implementando Gestión de Estado tipo Redux

En bibliotecas de gestión de estado como Redux, la inmutabilidad es un principio fundamental. Los tipos readonly pueden usarse para asegurar que el estado permanezca inmutable y que los reductores (reducers) solo devuelvan nuevos objetos de estado en lugar de modificar los existentes.


interface State {
  readonly count: number;
  readonly items: readonly string[];
}

const initialState: State = {
  count: 0,
  items: [],
};

function reducer(state: Readonly<State>, action: { type: string; payload?: any }): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // Devuelve un nuevo objeto de estado
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] }; // Devuelve un nuevo objeto de estado con items actualizados
    default:
      return state;
  }
}

3. Trabajando con Respuestas de API

Al obtener datos de una API, a menudo es deseable tratar los datos de respuesta como inmutables, especialmente si se están usando para renderizar componentes de la interfaz de usuario. Los tipos readonly pueden ayudar a prevenir mutaciones accidentales de los datos de la API.


interface ApiResponse {
  readonly userId: number;
  readonly id: number;
  readonly title: string;
  readonly completed: boolean;
}

async function fetchTodo(id: number): Promise<Readonly<ApiResponse>> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const data: ApiResponse = await response.json();
  return data;
}

fetchTodo(1).then(todo => {
  console.log(todo.title);
  // todo.completed = true; // Error: No se puede asignar a 'completed' porque es una propiedad de solo lectura.
});

4. Modelando Datos Geográficos (Ejemplo Internacional)

Consideremos la representación de coordenadas geográficas. Una vez que se establece una coordenada, idealmente debería permanecer constante. Esto asegura la integridad de los datos, particularmente cuando se trata de aplicaciones sensibles como sistemas de mapeo o navegación que operan en diferentes regiones geográficas (por ejemplo, coordenadas GPS para un servicio de entrega que abarca América del Norte, Europa y Asia).


interface GeoCoordinates {
 readonly latitude: number;
 readonly longitude: number;
}

const tokyoCoordinates: GeoCoordinates = {
 latitude: 35.6895,
 longitude: 139.6917
};

const newYorkCoordinates: GeoCoordinates = {
 latitude: 40.7128,
 longitude: -74.0060
};


function calculateDistance(coord1: Readonly<GeoCoordinates>, coord2: Readonly<GeoCoordinates>): number {
 // Imagina un cálculo complejo usando latitud y longitud
 // Devolviendo un valor de marcador de posición por simplicidad
 return 1000; 
}

const distance = calculateDistance(tokyoCoordinates, newYorkCoordinates);
console.log("Distancia entre Tokio y Nueva York (marcador de posición):", distance);

// tokyoCoordinates.latitude = 36.0; // Error: No se puede asignar a 'latitude' porque es una propiedad de solo lectura.

Tipos Profundamente Readonly: Manejando Objetos Anidados

El tipo de utilidad Readonly<T> solo hace que las propiedades directas de un objeto sean readonly. Si un objeto contiene objetos o arrays anidados, esas estructuras anidadas permanecen mutables. Para lograr una verdadera inmutabilidad profunda, necesitas aplicar recursivamente Readonly<T> a todas las propiedades anidadas.

Aquí hay un ejemplo de cómo crear un tipo profundamente readonly:


type DeepReadonly<T> = T extends (infer R)[]
  ? DeepReadonlyArray<R>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
  employees: string[];
}

const company: DeepReadonly<Company> = {
  name: "Example Corp",
  address: {
    street: "123 Main St",
    city: "Anytown",
    country: "USA",
  },
  employees: ["Alice", "Bob"],
};

// company.name = "New Corp"; // Error
// company.address.city = "New City"; // Error
// company.employees.push("Charlie"); // Error

Este tipo DeepReadonly<T> aplica recursivamente Readonly<T> a todas las propiedades anidadas, asegurando que toda la estructura del objeto sea inmutable.

Consideraciones y Compensaciones

Aunque la inmutabilidad ofrece beneficios significativos, es importante ser consciente de las posibles compensaciones.

Bibliotecas para Estructuras de Datos Inmutables

Varias bibliotecas pueden simplificar el trabajo con estructuras de datos inmutables en TypeScript:

Mejores Prácticas para Usar Tipos Readonly

Para aprovechar eficazmente los tipos readonly en tus proyectos de TypeScript, sigue estas mejores prácticas:

Conclusión: Adoptando la Inmutabilidad con los Tipos Readonly de TypeScript

Los tipos readonly de TypeScript son una herramienta poderosa para construir aplicaciones más predecibles, mantenibles y robustas. Al adoptar la inmutabilidad, puedes reducir el riesgo de errores, simplificar la depuración y mejorar la calidad general de tu código. Aunque hay algunas compensaciones a considerar, los beneficios de la inmutabilidad a menudo superan los costos, especialmente en proyectos complejos y de larga duración. A medida que continúes tu viaje con TypeScript, haz de los tipos readonly una parte central de tu flujo de trabajo de desarrollo para desbloquear todo el potencial de la inmutabilidad y construir software verdaderamente fiable.