Español

Explora los tipos literales de TypeScript, una potente función para aplicar restricciones de valor estrictas, mejorar la claridad del código y prevenir errores. Aprende con ejemplos prácticos y técnicas avanzadas.

Tipos Literales en TypeScript: Dominando las Restricciones de Valor Exacto

TypeScript, un superconjunto de JavaScript, aporta el tipado estático al dinámico mundo del desarrollo web. Una de sus características más potentes es el concepto de tipos literales. Los tipos literales te permiten especificar el valor exacto que una variable o propiedad puede contener, proporcionando una mayor seguridad de tipos y previniendo errores inesperados. Este artículo explorará los tipos literales en profundidad, cubriendo su sintaxis, uso y beneficios con ejemplos prácticos.

¿Qué son los Tipos Literales?

A diferencia de los tipos tradicionales como string, number o boolean, los tipos literales no representan una categoría amplia de valores. En su lugar, representan valores específicos y fijos. TypeScript admite tres clases de tipos literales:

Al usar tipos literales, puedes crear definiciones de tipo más precisas que reflejen las restricciones reales de tus datos, lo que conduce a un código más robusto y mantenible.

Tipos Literales de Cadena

Los tipos literales de cadena son el tipo de literal más comúnmente utilizado. Te permiten especificar que una variable o propiedad solo puede contener uno de un conjunto predefinido de valores de cadena.

Sintaxis Básica

La sintaxis para definir un tipo literal de cadena es sencilla:


type AllowedValues = "value1" | "value2" | "value3";

Esto define un tipo llamado AllowedValues que solo puede contener las cadenas "value1", "value2" o "value3".

Ejemplos Prácticos

1. Definir una Paleta de Colores:

Imagina que estás construyendo una biblioteca de UI y quieres asegurarte de que los usuarios solo puedan especificar colores de una paleta predefinida:


type Color = "red" | "green" | "blue" | "yellow";

function paintElement(element: HTMLElement, color: Color) {
  element.style.backgroundColor = color;
}

paintElement(document.getElementById("myElement")!, "red"); // Válido
paintElement(document.getElementById("myElement")!, "purple"); // Error: El argumento de tipo '"purple"' no es asignable al parámetro de tipo 'Color'.

Este ejemplo demuestra cómo los tipos literales de cadena pueden hacer cumplir un conjunto estricto de valores permitidos, evitando que los desarrolladores usen accidentalmente colores no válidos.

2. Definir Endpoints de API:

Cuando trabajas con APIs, a menudo necesitas especificar los endpoints permitidos. Los tipos literales de cadena pueden ayudar a hacer cumplir esto:


type APIEndpoint = "/users" | "/posts" | "/comments";

function fetchData(endpoint: APIEndpoint) {
  // ... implementación para obtener datos del endpoint especificado
  console.log(`Obteniendo datos de ${endpoint}`);
}

fetchData("/users"); // Válido
fetchData("/products"); // Error: El argumento de tipo '"/products"' no es asignable al parámetro de tipo 'APIEndpoint'.

Este ejemplo asegura que la función fetchData solo pueda ser llamada con endpoints de API válidos, reduciendo el riesgo de errores causados por erratas o nombres de endpoint incorrectos.

3. Manejar Diferentes Idiomas (Internacionalización - i18n):

En aplicaciones globales, podrías necesitar manejar diferentes idiomas. Puedes usar tipos literales de cadena para asegurar que tu aplicación solo soporte los idiomas especificados:


type Language = "en" | "es" | "fr" | "de" | "zh";

function translate(text: string, language: Language): string {
  // ... implementación para traducir el texto al idioma especificado
  console.log(`Traduciendo '${text}' a ${language}`);
  return "Texto traducido"; // Marcador de posición
}

translate("Hello", "en"); // Válido
translate("Hello", "ja"); // Error: El argumento de tipo '"ja"' no es asignable al parámetro de tipo 'Language'.

Este ejemplo demuestra cómo asegurar que solo se usen los idiomas soportados dentro de tu aplicación.

Tipos Literales Numéricos

Los tipos literales numéricos te permiten especificar que una variable o propiedad solo puede contener un valor numérico específico.

Sintaxis Básica

La sintaxis para definir un tipo literal numérico es similar a la de los tipos literales de cadena:


type StatusCode = 200 | 404 | 500;

Esto define un tipo llamado StatusCode que solo puede contener los números 200, 404 o 500.

Ejemplos Prácticos

1. Definir Códigos de Estado HTTP:

Puedes usar tipos literales numéricos para representar códigos de estado HTTP, asegurando que solo se usen códigos válidos en tu aplicación:


type HTTPStatus = 200 | 400 | 401 | 403 | 404 | 500;

function handleResponse(status: HTTPStatus) {
  switch (status) {
    case 200:
      console.log("¡Éxito!");
      break;
    case 400:
      console.log("Bad Request");
      break;
    // ... otros casos
    default:
      console.log("Estado Desconocido");
  }
}

handleResponse(200); // Válido
handleResponse(600); // Error: El argumento de tipo '600' no es asignable al parámetro de tipo 'HTTPStatus'.

Este ejemplo impone el uso de códigos de estado HTTP válidos, previniendo errores causados por el uso de códigos incorrectos o no estándar.

2. Representar Opciones Fijas:

Puedes usar tipos literales numéricos para representar opciones fijas en un objeto de configuración:


type RetryAttempts = 1 | 3 | 5;

interface Config {
  retryAttempts: RetryAttempts;
}

const config1: Config = { retryAttempts: 3 }; // Válido
const config2: Config = { retryAttempts: 7 }; // Error: El tipo '{ retryAttempts: 7; }' no es asignable al tipo 'Config'.

Este ejemplo limita los valores posibles para retryAttempts a un conjunto específico, mejorando la claridad y fiabilidad de tu configuración.

Tipos Literales Booleanos

Los tipos literales booleanos representan los valores específicos true o false. Aunque pueden parecer menos versátiles que los tipos literales de cadena o numéricos, pueden ser útiles en escenarios específicos.

Sintaxis Básica

La sintaxis para definir un tipo literal booleano es:


type IsEnabled = true | false;

Sin embargo, usar directamente true | false es redundante porque es equivalente al tipo boolean. Los tipos literales booleanos son más útiles cuando se combinan con otros tipos o en tipos condicionales.

Ejemplos Prácticos

1. Lógica Condicional con Configuración:

Puedes usar tipos literales booleanos para controlar el comportamiento de una función basado en una bandera de configuración:


interface FeatureFlags {
  darkMode: boolean;
  newUserFlow: boolean;
}

function initializeApp(flags: FeatureFlags) {
  if (flags.darkMode) {
    // Habilitar modo oscuro
    console.log("Habilitando modo oscuro...");
  } else {
    // Usar modo claro
    console.log("Usando modo claro...");
  }

  if (flags.newUserFlow) {
    // Habilitar flujo de nuevo usuario
    console.log("Habilitando flujo de nuevo usuario...");
  } else {
    // Usar flujo de usuario antiguo
    console.log("Usando flujo de usuario antiguo...");
  }
}

initializeApp({ darkMode: true, newUserFlow: false });

Aunque este ejemplo usa el tipo boolean estándar, podrías combinarlo con tipos condicionales (explicados más adelante) para crear un comportamiento más complejo.

2. Uniones Discriminadas:

Los tipos literales booleanos pueden usarse como discriminadores en tipos de unión. Considera el siguiente ejemplo:


interface SuccessResult {
  success: true;
  data: any;
}

interface ErrorResult {
  success: false;
  error: string;
}

type Result = SuccessResult | ErrorResult;

function processResult(result: Result) {
  if (result.success) {
    console.log("Éxito:", result.data);
  } else {
    console.error("Error:", result.error);
  }
}

processResult({ success: true, data: { name: "John" } });
processResult({ success: false, error: "Fallo al obtener los datos" });

Aquí, la propiedad success, que es un tipo literal booleano, actúa como un discriminador, permitiendo a TypeScript acotar el tipo de result dentro de la declaración if.

Combinando Tipos Literales con Tipos de Unión

Los tipos literales son más potentes cuando se combinan con tipos de unión (usando el operador |). Esto te permite definir un tipo que puede contener uno de varios valores específicos.

Ejemplos Prácticos

1. Definir un Tipo de Estado:


type Status = "pending" | "in progress" | "completed" | "failed";

interface Task {
  id: number;
  description: string;
  status: Status;
}

const task1: Task = { id: 1, description: "Implementar login", status: "in progress" }; // Válido
const task2: Task = { id: 2, description: "Implementar logout", status: "done" };       // Error: El tipo '{ id: number; description: string; status: string; }' no es asignable al tipo 'Task'.

Este ejemplo demuestra cómo hacer cumplir un conjunto específico de valores de estado permitidos para un objeto Task.

2. Definir un Tipo de Dispositivo:

En una aplicación móvil, podrías necesitar manejar diferentes tipos de dispositivos. Puedes usar una unión de tipos literales de cadena para representarlos:


type DeviceType = "mobile" | "tablet" | "desktop";

function logDeviceType(device: DeviceType) {
  console.log(`Tipo de dispositivo: ${device}`);
}

logDeviceType("mobile"); // Válido
logDeviceType("smartwatch"); // Error: El argumento de tipo '"smartwatch"' no es asignable al parámetro de tipo 'DeviceType'.

Este ejemplo asegura que la función logDeviceType solo se llame con tipos de dispositivo válidos.

Tipos Literales con Alias de Tipo

Los alias de tipo (usando la palabra clave type) proporcionan una forma de dar un nombre a un tipo literal, haciendo tu código más legible y mantenible.

Ejemplos Prácticos

1. Definir un Tipo de Código de Moneda:


type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";

function formatCurrency(amount: number, currency: CurrencyCode): string {
  // ... implementación para formatear la cantidad según el código de moneda
  console.log(`Formateando ${amount} en ${currency}`);
  return "Cantidad formateada"; // Marcador de posición
}

formatCurrency(100, "USD"); // Válido
formatCurrency(200, "CAD"); // Error: El argumento de tipo '"CAD"' no es asignable al parámetro de tipo 'CurrencyCode'.

Este ejemplo define un alias de tipo CurrencyCode para un conjunto de códigos de moneda, mejorando la legibilidad de la función formatCurrency.

2. Definir un Tipo de Día de la Semana:


type DayOfWeek = "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";

function isWeekend(day: DayOfWeek): boolean {
  return day === "Saturday" || day === "Sunday";
}

console.log(isWeekend("Monday"));   // false
console.log(isWeekend("Saturday")); // true
console.log(isWeekend("Funday"));   // Error: El argumento de tipo '"Funday"' no es asignable al parámetro de tipo 'DayOfWeek'.

Inferencia Literal

TypeScript a menudo puede inferir tipos literales automáticamente basándose en los valores que asignas a las variables. Esto es particularmente útil cuando se trabaja con variables const.

Ejemplos Prácticos

1. Inferencia de Tipos Literales de Cadena:


const apiKey = "your-api-key"; // TypeScript infiere que el tipo de apiKey es "your-api-key"

function validateApiKey(key: "your-api-key") {
  return key === "your-api-key";
}

console.log(validateApiKey(apiKey)); // true

const anotherKey = "invalid-key";
console.log(validateApiKey(anotherKey)); // Error: El argumento de tipo 'string' no es asignable al parámetro de tipo '"your-api-key"'.

En este ejemplo, TypeScript infiere el tipo de apiKey como el tipo literal de cadena "your-api-key". Sin embargo, si asignas un valor no constante a una variable, TypeScript generalmente inferirá el tipo más amplio string.

2. Inferencia de Tipos Literales Numéricos:


const port = 8080; // TypeScript infiere que el tipo de port es 8080

function startServer(portNumber: 8080) {
  console.log(`Iniciando servidor en el puerto ${portNumber}`);
}

startServer(port); // Válido

const anotherPort = 3000;
startServer(anotherPort); // Error: El argumento de tipo 'number' no es asignable al parámetro de tipo '8080'.

Uso de Tipos Literales con Tipos Condicionales

Los tipos literales se vuelven aún más potentes cuando se combinan con tipos condicionales. Los tipos condicionales te permiten definir tipos que dependen de otros tipos, creando sistemas de tipos muy flexibles y expresivos.

Sintaxis Básica

La sintaxis para un tipo condicional es:


TypeA extends TypeB ? TypeC : TypeD

Esto significa: si TypeA es asignable a TypeB, entonces el tipo resultante es TypeC; de lo contrario, el tipo resultante es TypeD.

Ejemplos Prácticos

1. Mapear Estado a Mensaje:


type Status = "pending" | "in progress" | "completed" | "failed";

type StatusMessage = T extends "pending"
  ? "Esperando acción"
  : T extends "in progress"
  ? "Procesando actualmente"
  : T extends "completed"
  ? "Tarea finalizada con éxito"
  : "Ocurrió un error";

function getStatusMessage(status: T): StatusMessage {
  switch (status) {
    case "pending":
      return "Esperando acción" as StatusMessage;
    case "in progress":
      return "Procesando actualmente" as StatusMessage;
    case "completed":
      return "Tarea finalizada con éxito" as StatusMessage;
    case "failed":
      return "Ocurrió un error" as StatusMessage;
    default:
      throw new Error("Estado inválido");
  }
}

console.log(getStatusMessage("pending"));    // Esperando acción
console.log(getStatusMessage("in progress")); // Procesando actualmente
console.log(getStatusMessage("completed"));   // Tarea finalizada con éxito
console.log(getStatusMessage("failed"));      // Ocurrió un error

Este ejemplo define un tipo StatusMessage que mapea cada estado posible a un mensaje correspondiente usando tipos condicionales. La función getStatusMessage aprovecha este tipo para proporcionar mensajes de estado con seguridad de tipos.

2. Crear un Manejador de Eventos con Seguridad de Tipos:


type EventType = "click" | "mouseover" | "keydown";

type EventData = T extends "click"
  ? { x: number; y: number; } // Datos del evento de clic
  : T extends "mouseover"
  ? { target: HTMLElement; }   // Datos del evento mouseover
  : { key: string; }             // Datos del evento keydown

function handleEvent(type: T, data: EventData) {
  console.log(`Manejando evento de tipo ${type} con datos:`, data);
}

handleEvent("click", { x: 10, y: 20 }); // Válido
handleEvent("mouseover", { target: document.getElementById("myElement")! }); // Válido
handleEvent("keydown", { key: "Enter" }); // Válido

handleEvent("click", { key: "Enter" }); // Error: El argumento de tipo '{ key: string; }' no es asignable al parámetro de tipo '{ x: number; y: number; }'.

Este ejemplo crea un tipo EventData que define diferentes estructuras de datos basadas en el tipo de evento. Esto te permite asegurar que se pasen los datos correctos a la función handleEvent para cada tipo de evento.

Mejores Prácticas para Usar Tipos Literales

Para usar eficazmente los tipos literales en tus proyectos de TypeScript, considera las siguientes mejores prácticas:

Beneficios de Usar Tipos Literales

Conclusión

Los tipos literales de TypeScript son una característica potente que te permite aplicar restricciones de valor estrictas, mejorar la claridad del código y prevenir errores. Al comprender su sintaxis, uso y beneficios, puedes aprovechar los tipos literales para crear aplicaciones de TypeScript más robustas y mantenibles. Desde la definición de paletas de colores y endpoints de API hasta el manejo de diferentes idiomas y la creación de manejadores de eventos con seguridad de tipos, los tipos literales ofrecen una amplia gama de aplicaciones prácticas que pueden mejorar significativamente tu flujo de trabajo de desarrollo.