TypeScript

TypeScript

Tutorial TypeScript: Tipos mapped

Aprende a usar mapped types en TypeScript para transformar tipos con modificadores, remapeo de claves y template literal types. Domina tipos avanzados y reutilizables.

Aprende TypeScript y certifícate

Sintaxis de mapped types

Los mapped types son una de las características más potentes de TypeScript que permiten crear nuevos tipos basados en tipos existentes mediante la transformación de sus propiedades. Esta técnica nos permite generar tipos derivados sin tener que definirlos manualmente propiedad por propiedad.

La sintaxis de los mapped types se basa en la notación de índices de TypeScript y utiliza una estructura similar a la de los tipos indexados. Veamos cómo funciona esta sintaxis paso a paso.

Estructura básica

La estructura básica de un mapped type sigue este patrón:

type MappedType<T> = {
  [P in keyof T]: T[P];
};

Analicemos cada parte:

  • [P in keyof T]: Esta es la cláusula de mapeo que itera sobre cada propiedad P del tipo T.
  • keyof T: Devuelve una unión de todas las claves (nombres de propiedades) del tipo T.
  • T[P]: Accede al tipo de la propiedad P en el tipo T.

Esta estructura básica crea un tipo idéntico al original, pero nos permite modificarlo de diferentes maneras.

Ejemplo práctico

Veamos un ejemplo sencillo para entender mejor cómo funciona:

interface Usuario {
  nombre: string;
  edad: number;
  email: string;
}

// Mapped type que hace todas las propiedades opcionales
type UsuarioOpcional = {
  [P in keyof Usuario]?: Usuario[P];
};

// Equivalente a:
// interface UsuarioOpcional {
//   nombre?: string;
//   edad?: number;
//   email?: string;
// }

En este ejemplo, hemos creado un nuevo tipo UsuarioOpcional donde todas las propiedades del tipo Usuario se han convertido en opcionales mediante el modificador ?.

Mapped types con tipos genéricos

Los mapped types son especialmente útiles cuando se combinan con tipos genéricos, permitiendo crear transformaciones reutilizables:

// Tipo genérico que hace todas las propiedades opcionales
type Parcial<T> = {
  [P in keyof T]?: T[P];
};

// Uso
interface Producto {
  id: number;
  nombre: string;
  precio: number;
  stock: number;
}

// Todas las propiedades son opcionales
type ProductoActualizable = Parcial<Producto>;

// Podemos crear un objeto con solo algunas propiedades
const actualizacion: ProductoActualizable = {
  precio: 99.99,
  stock: 50
};

De hecho, TypeScript incluye este tipo Partial<T> como parte de sus tipos de utilidad predefinidos.

Transformaciones complejas

Podemos crear transformaciones más complejas combinando diferentes técnicas:

// Tipo original
interface Configuracion {
  readonly apiUrl: string;
  timeout: number;
  retryCount: number;
}

// Mapped type que convierte todas las propiedades a solo lectura y string
type ConfiguracionString = {
  readonly [P in keyof Configuracion]: string;
};

// Resultado equivalente a:
// interface ConfiguracionString {
//   readonly apiUrl: string;
//   readonly timeout: string;
//   readonly retryCount: string;
// }

Mapped types condicionales

Podemos combinar mapped types con tipos condicionales para crear transformaciones más sofisticadas:

// Tipo que convierte números a strings y mantiene los demás tipos igual
type ConvertirNumerosAStrings<T> = {
  [P in keyof T]: T[P] extends number ? string : T[P];
};

interface Producto {
  id: number;
  nombre: string;
  precio: number;
  disponible: boolean;
}

// Uso del mapped type condicional
type ProductoConStrings = ConvertirNumerosAStrings<Producto>;

// Resultado equivalente a:
// interface ProductoConStrings {
//   id: string;
//   nombre: string;
//   precio: string;
//   disponible: boolean;
// }

Mapped types con tipos literales

También podemos usar tipos literales dentro de mapped types para crear transformaciones específicas:

type Metodos = 'GET' | 'POST' | 'PUT' | 'DELETE';

// Crea un objeto con métodos HTTP como claves y funciones como valores
type ApiEndpoints<T> = {
  [M in Metodos]: (url: string, data?: T) => Promise<T>;
};

interface Usuario {
  id: number;
  nombre: string;
}

// Uso
const apiUsuarios: ApiEndpoints<Usuario> = {
  GET: async (url) => ({ id: 1, nombre: "Ana" }),
  POST: async (url, data) => data!,
  PUT: async (url, data) => data!,
  DELETE: async (url) => ({ id: 1, nombre: "Ana" })
};

Mapped types anidados

Podemos crear mapped types que transforman propiedades anidadas:

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

interface Empresa {
  nombre: string;
  direccion: {
    calle: string;
    ciudad: string;
    codigoPostal: number;
  };
  empleados: {
    id: number;
    nombre: string;
  }[];
}

// Uso
type EmpresaInmutable = DeepReadonly<Empresa>;

// Todas las propiedades y subpropiedades son readonly
const empresa: EmpresaInmutable = {
  nombre: "TechCorp",
  direccion: {
    calle: "Calle Principal 123",
    ciudad: "Madrid",
    codigoPostal: 28001
  },
  empleados: [
    { id: 1, nombre: "Carlos" },
    { id: 2, nombre: "Laura" }
  ]
};

// Error: no se puede modificar una propiedad de solo lectura
// empresa.nombre = "NuevoNombre"; // Error
// empresa.direccion.ciudad = "Barcelona"; // Error
// empresa.empleados[0].nombre = "Carlos Modificado"; // Error

Homogeneización de tipos

Los mapped types son excelentes para homogeneizar estructuras de datos:

// Convierte todas las propiedades a un mismo tipo
type Homogeneizar<T, U> = {
  [P in keyof T]: U;
};

interface Formulario {
  nombre: string;
  edad: number;
  activo: boolean;
}

// Convierte todas las propiedades a string
type FormularioString = Homogeneizar<Formulario, string>;

// Equivalente a:
// interface FormularioString {
//   nombre: string;
//   edad: string;
//   activo: string;
// }

Los mapped types son una herramienta fundamental en TypeScript para crear transformaciones de tipos de forma declarativa y reutilizable. Dominando su sintaxis, podrás crear tipos complejos y flexibles que se adapten a las necesidades específicas de tus aplicaciones, mejorando la seguridad de tipos y reduciendo la duplicación de código.

Modificadores de propiedades

Los mapped types en TypeScript no solo permiten transformar los tipos de las propiedades, sino también modificar sus características mediante modificadores especiales. Estos modificadores nos dan control sobre si las propiedades son opcionales, de solo lectura, o ninguna de las anteriores.

TypeScript proporciona dos modificadores principales que podemos aplicar a las propiedades en un mapped type:

  • El modificador de opcionalidad (?)
  • El modificador de solo lectura (readonly)

Lo interesante es que podemos añadir o eliminar estos modificadores utilizando los operadores + y - respectivamente.

Añadir y quitar el modificador opcional

Podemos hacer que todas las propiedades de un tipo sean opcionales añadiendo el modificador ?:

type HacerOpcional<T> = {
  [P in keyof T]?: T[P];
};

interface Empleado {
  id: number;
  nombre: string;
  departamento: string;
}

// Todas las propiedades son opcionales
type EmpleadoOpcional = HacerOpcional<Empleado>;

// Uso válido
const nuevoEmpleado: EmpleadoOpcional = {
  nombre: "Ana" // No necesitamos proporcionar id ni departamento
};

También podemos eliminar explícitamente el modificador opcional de las propiedades usando el operador -:

type HacerRequerido<T> = {
  [P in keyof T]-?: T[P];
};

interface ConfiguracionParcial {
  servidor?: string;
  puerto?: number;
  timeout?: number;
}

// Todas las propiedades son requeridas
type ConfiguracionCompleta = HacerRequerido<ConfiguracionParcial>;

// Error: falta la propiedad 'timeout'
// const config: ConfiguracionCompleta = {
//   servidor: "api.ejemplo.com",
//   puerto: 8080
// };

Añadir y quitar el modificador readonly

De manera similar, podemos hacer que todas las propiedades sean de solo lectura:

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

interface Producto {
  id: number;
  nombre: string;
  precio: number;
}

// Todas las propiedades son de solo lectura
type ProductoInmutable = HacerSoloLectura<Producto>;

const producto: ProductoInmutable = {
  id: 1,
  nombre: "Teclado",
  precio: 49.99
};

// Error: no se puede asignar a 'precio' porque es una propiedad de solo lectura
// producto.precio = 39.99;

Y también podemos eliminar el modificador readonly usando el operador -:

type HacerMutable<T> = {
  -readonly [P in keyof T]: T[P];
};

interface ConfigInmutable {
  readonly apiKey: string;
  readonly endpoint: string;
}

// Todas las propiedades son mutables
type ConfigMutable = HacerMutable<ConfigInmutable>;

const config: ConfigMutable = {
  apiKey: "abc123",
  endpoint: "/api/v1"
};

// Ahora es válido modificar las propiedades
config.apiKey = "xyz789";

Combinando modificadores

Podemos combinar ambos modificadores para crear transformaciones más complejas:

type BloquearConfig<T> = {
  readonly [P in keyof T]-?: T[P];
};

interface ConfiguracionApp {
  tema?: string;
  idioma?: string;
  notificaciones?: boolean;
}

// Propiedades requeridas y de solo lectura
type ConfiguracionBloqueada = BloquearConfig<ConfiguracionApp>;

// Debe incluir todas las propiedades y no se pueden modificar después
const appConfig: ConfiguracionBloqueada = {
  tema: "oscuro",
  idioma: "es",
  notificaciones: true
};

// Error: no se puede modificar una propiedad de solo lectura
// appConfig.tema = "claro";

Modificadores con tipos genéricos

Los modificadores son especialmente útiles cuando se combinan con tipos genéricos para crear utilidades reutilizables:

// Hace que solo ciertas propiedades sean opcionales
type OpcionalSelectivo<T, K extends keyof T> = {
  [P in keyof T]: P extends K ? T[P] | undefined : T[P];
};

interface Usuario {
  id: number;
  nombre: string;
  email: string;
  telefono: string;
}

// Solo 'email' y 'telefono' son opcionales
type UsuarioRegistro = OpcionalSelectivo<Usuario, 'email' | 'telefono'>;

// Válido: podemos omitir email y telefono
const nuevoUsuario: UsuarioRegistro = {
  id: 1,
  nombre: "Carlos"
};

Modificadores con tipos condicionales

Podemos aplicar modificadores de forma condicional basándonos en los tipos de las propiedades:

// Hace que solo las propiedades de tipo string sean de solo lectura
type SoloLecturaStrings<T> = {
  [P in keyof T]: T[P] extends string
    ? readonly T[P]
    : T[P];
};

interface Documento {
  id: number;
  titulo: string;
  contenido: string;
  fechaCreacion: Date;
}

// Solo las propiedades de tipo string son readonly
type DocumentoProtegido = SoloLecturaStrings<Documento>;

Aplicaciones prácticas

Los modificadores de propiedades son extremadamente útiles en escenarios reales de desarrollo:

Creación de tipos para actualizaciones parciales

// Tipo para actualización parcial de entidades
type ActualizacionParcial<T> = {
  [P in keyof T]?: T[P];
};

interface Articulo {
  id: number;
  titulo: string;
  contenido: string;
  autor: string;
  fechaPublicacion: Date;
}

// Función que actualiza un artículo
function actualizarArticulo(id: number, datos: ActualizacionParcial<Articulo>) {
  // Implementación...
}

// Uso: solo actualizamos el título y contenido
actualizarArticulo(123, {
  titulo: "Nuevo título",
  contenido: "Contenido actualizado"
});

Creación de tipos inmutables para estado

// Hace todas las propiedades y subpropiedades inmutables
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

interface EstadoAplicacion {
  usuario: {
    id: number;
    nombre: string;
    preferencias: {
      tema: string;
      notificaciones: boolean;
    };
  };
  sesion: {
    token: string;
    expiracion: Date;
  };
}

// Estado completamente inmutable
type EstadoInmutable = DeepReadonly<EstadoAplicacion>;

Validación de formularios

// Convierte un tipo en un objeto de errores de validación
type ErroresValidacion<T> = {
  [P in keyof T]?: string;
};

interface FormularioContacto {
  nombre: string;
  email: string;
  mensaje: string;
}

// Tipo para almacenar errores de validación
type ErroresFormularioContacto = ErroresValidacion<FormularioContacto>;

// Uso
const errores: ErroresFormularioContacto = {
  email: "El email no es válido",
  mensaje: "El mensaje es demasiado corto"
};

Los modificadores de propiedades en mapped types proporcionan una forma elegante y concisa de transformar tipos existentes, permitiéndonos crear tipos derivados que se adapten perfectamente a nuestras necesidades específicas. Esta capacidad es fundamental para desarrollar sistemas de tipos robustos y expresivos en aplicaciones TypeScript complejas.

Key remapping

El key remapping (remapeo de claves) es una característica avanzada de los mapped types en TypeScript que nos permite no solo transformar los tipos de las propiedades, sino también modificar los nombres de las propiedades durante el mapeo. Esta funcionalidad, introducida en TypeScript 4.1, amplía significativamente el poder de los mapped types.

La sintaxis para el remapeo de claves utiliza la palabra clave as dentro de la cláusula de mapeo:

type MappedType<T> = {
  [P in keyof T as NewKeyType]: T[P];
};

Donde NewKeyType es una expresión de tipo que determina el nuevo nombre de la propiedad.

Transformación básica de nombres de propiedades

Podemos usar el remapeo de claves para crear nuevos nombres de propiedades basados en los originales:

type PrefixProps<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

interface Usuario {
  nombre: string;
  edad: number;
  email: string;
}

// Añade el prefijo "user" a todas las propiedades
type UserProps = PrefixProps<Usuario, "user">;

// Resultado equivalente a:
// {
//   userNombre: string;
//   userEdad: number;
//   userEmail: string;
// }

const usuario: UserProps = {
  userNombre: "Ana",
  userEdad: 28,
  userEmail: "ana@ejemplo.com"
};

En este ejemplo, cada propiedad del tipo original se transforma añadiéndole un prefijo.

Filtrado de propiedades mediante remapeo

Una aplicación poderosa del remapeo de claves es la capacidad de filtrar propiedades. Podemos devolver never para excluir propiedades específicas:

type OmitByType<T, U> = {
  [P in keyof T as T[P] extends U ? never : P]: T[P];
};

interface Producto {
  id: number;
  nombre: string;
  precio: number;
  descripcion: string;
  enStock: boolean;
}

// Excluye todas las propiedades de tipo string
type ProductoSinTexto = OmitByType<Producto, string>;

// Resultado equivalente a:
// {
//   id: number;
//   precio: number;
//   enStock: boolean;
// }

const productoReducido: ProductoSinTexto = {
  id: 1,
  precio: 29.99,
  enStock: true
};

En este caso, estamos filtrando todas las propiedades de tipo string al mapearlas a never, lo que hace que se excluyan del tipo resultante.

Creación de tipos de métodos a partir de propiedades

El remapeo de claves también nos permite transformar propiedades en métodos:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Persona {
  nombre: string;
  edad: number;
  activo: boolean;
}

// Crea métodos getter para cada propiedad
type PersonaGetters = Getters<Persona>;

// Resultado equivalente a:
// {
//   getNombre: () => string;
//   getEdad: () => number;
//   getActivo: () => boolean;
// }

const personaAccesors: PersonaGetters = {
  getNombre: () => "Carlos",
  getEdad: () => 35,
  getActivo: () => true
};

console.log(personaAccesors.getNombre()); // "Carlos"

Aquí estamos creando métodos getter para cada propiedad, capitalizando el nombre de la propiedad y añadiendo el prefijo "get".

Remapeo condicional de claves

Podemos aplicar lógica condicional para determinar qué nombres de propiedades transformar:

type ConditionalRemap<T> = {
  [K in keyof T as K extends `is${string}` ? K : `get${Capitalize<string & K>}`]: T[K];
};

interface EstadoUsuario {
  nombre: string;
  edad: number;
  isActivo: boolean;
  isAdmin: boolean;
}

// Mantiene propiedades que comienzan con "is", transforma el resto
type EstadoUsuarioAPI = ConditionalRemap<EstadoUsuario>;

// Resultado equivalente a:
// {
//   getNombre: string;
//   getEdad: number;
//   isActivo: boolean;
//   isAdmin: boolean;
// }

En este ejemplo, mantenemos las propiedades que ya comienzan con "is" sin cambios, mientras que transformamos las demás añadiéndoles el prefijo "get" y capitalizando su nombre.

Extracción de subconjuntos de propiedades

El remapeo de claves es especialmente útil para extraer subconjuntos específicos de propiedades:

type ExtractByValueType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Formulario {
  nombre: string;
  email: string;
  edad: number;
  fechaNacimiento: Date;
  activo: boolean;
  visitas: number;
}

// Extrae solo las propiedades numéricas
type CamposNumericos = ExtractByValueType<Formulario, number>;

// Resultado equivalente a:
// {
//   edad: number;
//   visitas: number;
// }

const estadisticas: CamposNumericos = {
  edad: 28,
  visitas: 45
};

Este patrón es muy útil para crear tipos específicos basados en los tipos de valores de las propiedades.

Transformación de objetos a uniones de tuplas

Una aplicación avanzada del remapeo de claves es la transformación de objetos en uniones de tuplas:

type ObjectToTuples<T> = {
  [K in keyof T]: [K, T[K]];
}[keyof T];

interface Configuracion {
  servidor: string;
  puerto: number;
  ssl: boolean;
}

// Convierte el objeto en una unión de tuplas [clave, valor]
type ConfigTuples = ObjectToTuples<Configuracion>;

// Resultado equivalente a:
// ["servidor", string] | ["puerto", number] | ["ssl", boolean]

// Uso
const configEntries: ConfigTuples[] = [
  ["servidor", "api.ejemplo.com"],
  ["puerto", 443],
  ["ssl", true]
];

Esta técnica es útil para trabajar con estructuras de datos que requieren pares clave-valor, como en operaciones de mapeo o reducción.

Aplicaciones prácticas en desarrollo

Creación de tipos para APIs RESTful

type HttpMethods = "GET" | "POST" | "PUT" | "DELETE";

type ApiEndpoints<T extends Record<string, any>> = {
  [K in keyof T as `${HttpMethods}:${string & K}`]: T[K];
};

interface Recursos {
  "/usuarios": { id: number; nombre: string }[];
  "/productos": { id: number; nombre: string; precio: number }[];
  "/pedidos": { id: number; usuarioId: number; productos: number[] }[];
}

// Genera endpoints para cada recurso y método HTTP
type API = ApiEndpoints<Recursos>;

// Uso
const api: API = {
  "GET:/usuarios": [{ id: 1, nombre: "Ana" }],
  "POST:/usuarios": [{ id: 2, nombre: "Carlos" }],
  "PUT:/usuarios": [{ id: 1, nombre: "Ana Actualizada" }],
  "DELETE:/usuarios": [{ id: 2, nombre: "Carlos" }],
  "GET:/productos": [{ id: 1, nombre: "Laptop", precio: 999 }],
  "POST:/productos": [{ id: 2, nombre: "Teléfono", precio: 699 }],
  "PUT:/productos": [{ id: 1, nombre: "Laptop Pro", precio: 1299 }],
  "DELETE:/productos": [{ id: 2, nombre: "Teléfono", precio: 699 }],
  "GET:/pedidos": [{ id: 1, usuarioId: 1, productos: [1, 2] }],
  "POST:/pedidos": [{ id: 2, usuarioId: 2, productos: [1] }],
  "PUT:/pedidos": [{ id: 1, usuarioId: 1, productos: [1, 2, 3] }],
  "DELETE:/pedidos": [{ id: 2, usuarioId: 2, productos: [1] }]
};

Transformación de modelos para serialización

type Serializable<T> = {
  [K in keyof T as `_${string & K}`]: T[K] extends Function ? never : T[K] extends object ? Serializable<T[K]> : T[K];
};

class Usuario {
  id: number;
  nombre: string;
  private password: string;
  
  constructor(id: number, nombre: string, password: string) {
    this.id = id;
    this.nombre = nombre;
    this.password = password;
  }
  
  verificarPassword(input: string): boolean {
    return this.password === input;
  }
}

// Transforma la clase en un tipo serializable
type UsuarioSerializable = Serializable<Usuario>;

// Resultado aproximado:
// {
//   _id: number;
//   _nombre: string;
//   _password: string;
//   // El método verificarPassword se excluye
// }

function serializar<T>(objeto: T): Serializable<T> {
  const resultado: any = {};
  
  for (const key in objeto) {
    if (typeof objeto[key] !== 'function') {
      resultado[`_${key}`] = objeto[key];
    }
  }
  
  return resultado;
}

const usuario = new Usuario(1, "Ana", "secreto123");
const serializado = serializar(usuario);
console.log(serializado); // { _id: 1, _nombre: "Ana", _password: "secreto123" }

El remapeo de claves en mapped types proporciona una flexibilidad extraordinaria para transformar tipos en TypeScript. Esta característica nos permite crear tipos derivados con nombres de propiedades personalizados, filtrar propiedades basadas en criterios específicos y transformar estructuras de datos de manera declarativa. Dominar esta técnica es esencial para aprovechar al máximo el sistema de tipos de TypeScript y crear abstracciones de tipos potentes y expresivas.

Template literal types

Los template literal types son una característica introducida en TypeScript 4.1 que extiende el sistema de tipos con la capacidad de manipular tipos de cadenas de texto de forma similar a los template literals de JavaScript. Esta funcionalidad permite crear tipos de cadena complejos mediante la combinación de cadenas literales, uniones de tipos y otros tipos primitivos.

Sintaxis básica

La sintaxis de los template literal types es similar a la de los template literals de JavaScript, utilizando comillas invertidas (backticks) y expresiones de tipo entre ${ y }:

type Saludo = `Hola, ${string}`;

// Saludo puede ser cualquier cadena que comience con "Hola, "
const saludo1: Saludo = "Hola, mundo"; // Válido
const saludo2: Saludo = "Hola, TypeScript"; // Válido
// const saludo3: Saludo = "Buenos días"; // Error: no comienza con "Hola, "

En este ejemplo, Saludo es un tipo que representa cualquier cadena que comience con "Hola, " seguido de cualquier texto.

Combinación con uniones de tipos

Los template literal types son especialmente potentes cuando se combinan con uniones de tipos:

type Animal = "gato" | "perro" | "pájaro";
type Sonido = "maulla" | "ladra" | "canta";

type FraseAnimal = `El ${Animal} ${Sonido}`;

// FraseAnimal puede ser cualquiera de estas combinaciones:
// "El gato maulla", "El gato ladra", "El gato canta",
// "El perro maulla", "El perro ladra", "El perro canta",
// "El pájaro maulla", "El pájaro ladra", "El pájaro canta"

const frase1: FraseAnimal = "El gato maulla"; // Válido
const frase2: FraseAnimal = "El perro ladra"; // Válido
// const frase3: FraseAnimal = "El pez nada"; // Error: no es una combinación válida

TypeScript expandirá automáticamente las uniones dentro de los template literal types, creando una unión de todas las posibles combinaciones.

Manipulación de cadenas con tipos utilitarios intrínsecos

TypeScript proporciona varios tipos utilitarios intrínsecos para manipular tipos de cadenas:

  • Uppercase<T>: Convierte todas las letras a mayúsculas
  • Lowercase<T>: Convierte todas las letras a minúsculas
  • Capitalize<T>: Convierte la primera letra a mayúscula
  • Uncapitalize<T>: Convierte la primera letra a minúscula
type Minuscula = "hola mundo";
type Mayuscula = Uppercase<Minuscula>; // "HOLA MUNDO"

type Nombre = "juan";
type NombreCapitalizado = Capitalize<Nombre>; // "Juan"

type Evento = "click" | "scroll" | "mousemove";
type ManejadorEvento = `on${Capitalize<Evento>}`; // "onClick" | "onScroll" | "onMousemove"

// Uso
function registrarManejador(evento: Evento, manejador: () => void) {
  const elemento = document.getElementById("miElemento");
  const nombreManejador = `on${evento.charAt(0).toUpperCase()}${evento.slice(1)}` as ManejadorEvento;
  
  if (elemento) {
    (elemento as any)[nombreManejador] = manejador;
  }
}

Extracción de información de tipos de cadena

Los template literal types también se pueden usar con tipos condicionales para extraer información de cadenas:

type ExtractPart<T, P extends string> = 
  T extends `${infer Prefix}${P}${infer Suffix}` ? [Prefix, Suffix] : never;

// Extrae el texto antes y después de "-"
type Partes = ExtractPart<"usuario-123", "-">; // ["usuario", "123"]

// Uso práctico: extraer partes de un ID
type ParseUserId<T extends string> = 
  T extends `user-${infer Id}` ? Id : never;

type UserId = ParseUserId<"user-abc123">; // "abc123"

El operador infer permite capturar partes de un template literal type en variables de tipo que luego podemos utilizar.

Aplicaciones prácticas

Validación de rutas de API

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Route = "/users" | "/posts" | "/comments";

type Endpoint = `${HttpMethod} ${Route}`;

function fetchAPI(endpoint: Endpoint, data?: object) {
  const [method, route] = endpoint.split(" ") as [HttpMethod, Route];
  
  // Implementación...
  console.log(`Realizando petición ${method} a ${route}`);
}

// Uso válido
fetchAPI("GET /users");
fetchAPI("POST /posts", { title: "Nuevo post", content: "Contenido..." });

// Error: no es un endpoint válido
// fetchAPI("PATCH /users");
// fetchAPI("GET /products");

Creación de tipos para eventos personalizados

type AppEvent = "user" | "post" | "comment";
type EventAction = "create" | "update" | "delete";

type AppEventType = `${AppEvent}:${EventAction}`;

interface EventPayload<T extends AppEventType> {
  type: T;
  timestamp: number;
  data: any;
}

function dispatchEvent<T extends AppEventType>(event: EventPayload<T>) {
  console.log(`Evento disparado: ${event.type} a las ${new Date(event.timestamp).toISOString()}`);
  // Lógica para manejar el evento...
}

// Uso
dispatchEvent({
  type: "user:create",
  timestamp: Date.now(),
  data: { id: 1, name: "Ana" }
});

// Error: tipo de evento no válido
// dispatchEvent({
//   type: "product:create",
//   timestamp: Date.now(),
//   data: {}
// });

Generación de tipos para propiedades CSS

type CSSProperty = "margin" | "padding" | "border";
type CSSDirection = "top" | "right" | "bottom" | "left";

type CSSPropertyWithDirection = `${CSSProperty}${Capitalize<CSSDirection>}`;

interface CSSProperties {
  [key in CSSProperty | CSSPropertyWithDirection]?: string | number;
}

const styles: CSSProperties = {
  margin: "10px",
  marginTop: "20px",
  paddingLeft: 15,
  border: "1px solid black"
};

// Error: propiedad no válida
// const invalidStyles: CSSProperties = {
//   marginCenter: "10px"
// };

Creación de tipos para esquemas de bases de datos

type TableName = "users" | "posts" | "comments";
type Operation = "select" | "insert" | "update" | "delete";

type Query<T extends TableName, O extends Operation> = `${O} from ${T}`;

function executeQuery<T extends TableName, O extends Operation>(
  query: Query<T, O>,
  params?: Record<string, any>
) {
  const [operation, _, table] = query.split(" ");
  console.log(`Ejecutando ${operation} en tabla ${table}`);
  // Implementación...
}

// Uso
executeQuery("select from users", { where: { id: 1 } });
executeQuery("insert from posts", { data: { title: "Nuevo post" } });

// Error: operación o tabla no válida
// executeQuery("drop from users");
// executeQuery("select from products");

Los template literal types representan una poderosa adición al sistema de tipos de TypeScript, permitiendo crear tipos de cadena complejos y expresivos. Combinados con otras características como mapped types y conditional types, proporcionan herramientas sofisticadas para modelar y validar estructuras de datos basadas en cadenas de texto, mejorando la seguridad de tipos y la experiencia de desarrollo en aplicaciones TypeScript.

Template literal types

Los template literal types representan una de las características más innovadoras de TypeScript, permitiendo crear tipos basados en patrones de texto de forma similar a cómo funcionan los template literals en JavaScript. Esta funcionalidad, introducida en TypeScript 4.1, nos permite manipular y combinar tipos de cadenas de texto con una flexibilidad sin precedentes.

Creación de tipos basados en patrones

Los template literal types utilizan la misma sintaxis de backticks (`) que los template literals de JavaScript, pero a nivel de tipos:

// Tipo que representa cualquier cadena que comience con "error-"
type ErrorCode = `error-${string}`;

// Válido
const databaseError: ErrorCode = "error-database-connection";
const networkError: ErrorCode = "error-network-timeout";

// Error: no comienza con "error-"
// const invalidError: ErrorCode = "warning-disk-space";

Esta capacidad nos permite definir patrones específicos que deben seguir nuestras cadenas de texto, añadiendo una capa adicional de seguridad de tipos.

Combinaciones con tipos literales

Donde los template literal types realmente brillan es al combinarlos con uniones de tipos literales:

type Color = "red" | "green" | "blue";
type Size = "small" | "medium" | "large";

// Genera todas las combinaciones posibles
type ColorSize = `${Color}-${Size}`;

// ColorSize es equivalente a:
// "red-small" | "red-medium" | "red-large" | 
// "green-small" | "green-medium" | "green-large" | 
// "blue-small" | "blue-medium" | "blue-large"

const productVariant: ColorSize = "blue-medium"; // Válido
// const invalidVariant: ColorSize = "yellow-small"; // Error

TypeScript calcula automáticamente todas las combinaciones posibles, creando un tipo que representa exactamente esas opciones.

Aplicación con tipos numéricos y booleanos

Los template literal types también funcionan con tipos numéricos y booleanos, convirtiéndolos automáticamente a cadenas:

type Status = 200 | 404 | 500;
type ResponseType = `status-${Status}`;

// ResponseType es: "status-200" | "status-404" | "status-500"

type Toggle = true | false;
type FeatureFlag = `feature-${string}-enabled-${Toggle}`;

// Ejemplos válidos
const darkMode: FeatureFlag = "feature-darkMode-enabled-true";
const analytics: FeatureFlag = "feature-analytics-enabled-false";

Esta característica es especialmente útil para trabajar con códigos de estado, configuraciones y otros valores que tienen un conjunto finito de opciones.

Creación de tipos para eventos DOM

Los template literal types son perfectos para modelar sistemas de eventos:

type ElementType = "button" | "input" | "form";
type EventName = "click" | "change" | "submit" | "focus";

// Genera combinaciones específicas de elementos y eventos
type DOMEvent = `${ElementType}:${EventName}`;

function addEventListener(event: DOMEvent, callback: () => void) {
  const [element, eventName] = event.split(":");
  console.log(`Añadiendo listener para ${eventName} en ${element}`);
  // Implementación...
}

// Uso
addEventListener("button:click", () => console.log("Botón clicado"));
addEventListener("form:submit", () => console.log("Formulario enviado"));

// Error: combinación no válida
// addEventListener("div:click", () => {});

Este enfoque nos permite crear APIs tipadas que capturan exactamente las combinaciones permitidas.

Generación de tipos para APIs de configuración

Los template literal types son ideales para crear APIs de configuración con opciones específicas:

type Theme = "light" | "dark";
type Language = "es" | "en" | "fr";
type Unit = "metric" | "imperial";

type ConfigSetting = `config:${Theme}:${Language}:${Unit}`;

function setConfig(setting: ConfigSetting) {
  // Implementación...
  console.log(`Configuración aplicada: ${setting}`);
}

// Uso válido
setConfig("config:dark:en:metric");

// Error: valores no válidos
// setConfig("config:blue:en:metric");
// setConfig("config:dark:de:metric");

Inferencia de tipos con template literals

Podemos usar template literal types junto con el operador infer para extraer información de cadenas estructuradas:

type ExtractCoordinates<T extends string> = 
  T extends `point(${infer X},${infer Y})` ? [X, Y] : never;

// Extrae las coordenadas de una cadena con formato "point(x,y)"
type Coordinates = ExtractCoordinates<"point(10,20)">;  // ["10", "20"]

// Uso práctico
function parsePoint<T extends string>(
  point: T
): ExtractCoordinates<T> extends [string, string] 
  ? [number, number] 
  : never {
  
  const match = point.match(/point\((\d+),(\d+)\)/);
  if (!match) {
    throw new Error("Formato inválido");
  }
  
  return [parseInt(match[1]), parseInt(match[2])] as any;
}

const coords = parsePoint("point(10,20)");  // [10, 20]
console.log(coords[0], coords[1]);  // 10 20

Creación de tipos para rutas de API

Una aplicación práctica común es la validación de rutas de API:

type APIVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";
type Action = "get" | "create" | "update" | "delete";

type APIRoute = `/${APIVersion}/${Resource}/${Action}`;

function fetchAPI(route: APIRoute, data?: object) {
  console.log(`Realizando petición a ${route}`);
  // Implementación...
}

// Uso válido
fetchAPI("/v1/users/get");
fetchAPI("/v2/posts/create", { title: "Nuevo post" });

// Error: ruta no válida
// fetchAPI("/v3/users/get");
// fetchAPI("/v1/products/get");

Generación de tipos para sistemas de mensajería

Los template literal types son excelentes para sistemas de mensajería o eventos:

type Channel = "system" | "user" | "admin";
type MessageType = "info" | "warning" | "error";
type Priority = "low" | "medium" | "high";

type Message = `[${Channel}][${MessageType}][${Priority}] ${string}`;

function log(message: Message) {
  console.log(message);
  // Podríamos parsear el mensaje para extraer metadatos
}

// Uso válido
log("[system][error][high] Database connection failed");
log("[user][info][low] User logged in successfully");

// Error: formato no válido
// log("System error: database connection failed");
// log("[network][error][high] Connection timeout");

Integración con mapped types

Los template literal types se integran perfectamente con mapped types para crear transformaciones de tipos potentes:

type ModelActions<T extends string> = {
  [K in `${T}${"Create" | "Read" | "Update" | "Delete"}`]: () => void;
};

// Genera métodos CRUD para un modelo
function createModelAPI<T extends string>(modelName: T): ModelActions<T> {
  return {
    [`${modelName}Create`]: () => console.log(`Creating ${modelName}`),
    [`${modelName}Read`]: () => console.log(`Reading ${modelName}`),
    [`${modelName}Update`]: () => console.log(`Updating ${modelName}`),
    [`${modelName}Delete`]: () => console.log(`Deleting ${modelName}`)
  } as ModelActions<T>;
}

// Uso
const userAPI = createModelAPI("user");
userAPI.userCreate();  // "Creating user"
userAPI.userUpdate();  // "Updating user"

Creación de tipos para sistemas de validación

Los template literal types pueden usarse para crear sistemas de validación de formularios tipados:

type FieldName = "username" | "email" | "password";
type ValidationRule = "required" | "minLength" | "pattern";

type ValidationError = `${FieldName}:${ValidationRule}`;

interface ValidationResult {
  valid: boolean;
  errors: ValidationError[];
}

function validateForm(data: Record<FieldName, string>): ValidationResult {
  const errors: ValidationError[] = [];
  
  // Validación de ejemplo
  if (!data.username) {
    errors.push("username:required");
  }
  
  if (data.password.length < 8) {
    errors.push("password:minLength");
  }
  
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
    errors.push("email:pattern");
  }
  
  return {
    valid: errors.length === 0,
    errors
  };
}

// Uso
const result = validateForm({
  username: "john_doe",
  email: "invalid-email",
  password: "123"
});

// Procesamiento de errores tipado
result.errors.forEach(error => {
  const [field, rule] = error.split(":") as [FieldName, ValidationRule];
  console.log(`Error en ${field}: no cumple la regla ${rule}`);
});

Generación de tipos para sistemas de permisos

Los template literal types son ideales para modelar sistemas de permisos:

type Resource = "users" | "posts" | "comments";
type Permission = "read" | "write" | "delete";

type PermissionToken = `${Resource}:${Permission}`;

function checkPermission(userPermissions: PermissionToken[], requested: PermissionToken): boolean {
  return userPermissions.includes(requested);
}

// Permisos de un usuario
const userPermissions: PermissionToken[] = [
  "users:read",
  "posts:read",
  "posts:write",
  "comments:read"
];

// Comprobaciones
const canReadUsers = checkPermission(userPermissions, "users:read");  // true
const canDeletePosts = checkPermission(userPermissions, "posts:delete");  // false

// Error: permiso no válido
// checkPermission(userPermissions, "settings:read");

Los template literal types han transformado la forma en que modelamos y validamos cadenas de texto en TypeScript, permitiéndonos crear APIs tipadas más precisas y expresivas. Al combinarlos con otras características avanzadas de TypeScript como mapped types, conditional types y el operador infer, podemos crear sistemas de tipos sofisticados que capturan con precisión las restricciones y patrones de nuestros dominios de aplicación.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende TypeScript online

Ejercicios de esta lección Tipos mapped

Evalúa tus conocimientos de esta lección Tipos mapped con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Funciones

TypeScript
Test

Reto composición de funciones

TypeScript
Código

Reto tipos especiales

TypeScript
Código

Reto tipos genéricos

TypeScript
Código

Módulos

TypeScript
Test

Polimorfismo

TypeScript
Código

Funciones TypeScript

TypeScript
Código

Interfaces

TypeScript
Puzzle

Funciones puras

TypeScript
Puzzle

Reto namespaces

TypeScript
Código

Funciones flecha

TypeScript
Puzzle

Polimorfismo

TypeScript
Test

Operadores

TypeScript
Test

Conversor de unidades

TypeScript
Proyecto

Funciones flecha

TypeScript
Test

Control de flujo

TypeScript
Código

Herencia

TypeScript
Puzzle

Clases

TypeScript
Puzzle

Proyecto validación de tipado

TypeScript
Proyecto

Clases y objetos

TypeScript
Código

Encapsulación

TypeScript
Test

Herencia

TypeScript
Test

Proyecto sistema de votación

TypeScript
Proyecto

Reto genéricos con clases

TypeScript
Código

Inmutabilidad

TypeScript
Puzzle

Interfaces

TypeScript
Test

Funciones de alto orden

TypeScript
Test

Reto map y filter

TypeScript
Código

Control de flujo

TypeScript
Test

Interfaces

TypeScript
Código

Reto funciones orden superior

TypeScript
Código

Herencia y clases abstractas

TypeScript
Código

Reto tipos mapped

TypeScript
Código

Herencia de clases

TypeScript
Código

Reto funciones puras

TypeScript
Código

Variables y constantes

TypeScript
Puzzle

Introducción a TypeScript

TypeScript
Test

Reto testing unitario

TypeScript
Código

Funciones de primera clase

TypeScript
Puzzle

Clases

TypeScript
Test

OOP y CRUD en TypeScript

TypeScript
Proyecto

Interfaces y su implementación

TypeScript
Código

Tipos genéricos

TypeScript
Test

Namespaces

TypeScript
Test

Proyecto calculadora gastos

TypeScript
Proyecto

Operadores y expresiones

TypeScript
Código

Proyecto generador de contraseñas

TypeScript
Proyecto

Reto unión e intersección

TypeScript
Código

Encapsulación

TypeScript
Puzzle

Tipos de unión e intersección

TypeScript
Test

Tipos de unión e intersección

TypeScript
Puzzle

Reto hola mundo en TS

TypeScript
Código

Variables y constantes

TypeScript
Código

Funciones puras

TypeScript
Test

Control de flujo

TypeScript
Código

Introducción a TypeScript

TypeScript
Código

Resolución de módulos

TypeScript
Test

Control de flujo

TypeScript
Puzzle

Reto tipos de utilidad

TypeScript
Código

Reto tipos literales y condicionales

TypeScript
Código

Reto exportar e importar

TypeScript
Código

Propiedades y métodos

TypeScript
Código

Tipos de utilidad

TypeScript
Test

Clases y objetos

TypeScript
Código

Tipos de datos, variables y constantes

TypeScript
Código

Proyecto Minigestor de tareas

TypeScript
Proyecto

Operadores

TypeScript
Puzzle

Funciones flecha y contexto

TypeScript
Código

Funciones

TypeScript
Puzzle

Reto type aliases

TypeScript
Código

Funciones de alto orden

TypeScript
Puzzle

Funciones y parámetros tipados

TypeScript
Código

Tipos literales

TypeScript
Puzzle

Reto enums

TypeScript
Código

Tipos de utilidad

TypeScript
Puzzle

Modificadores de acceso y encapsulación

TypeScript
Código

Polimorfismo

TypeScript
Puzzle

Tipos genéricos

TypeScript
Puzzle

Reto módulos

TypeScript
Código

Tipos literales

TypeScript
Test

Inmutabilidad

TypeScript
Test

Proyecto Generator de datos

TypeScript
Proyecto

Variables y constantes

TypeScript
Test

Funciones de primera clase

TypeScript
Test

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

TypeScript

Introducción Y Entorno

Instalación Y Configuración De Typescript

TypeScript

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

TypeScript

Sintaxis

Operadores Y Expresiones

TypeScript

Sintaxis

Control De Flujo

TypeScript

Sintaxis

Funciones Y Parámetros Tipados

TypeScript

Sintaxis

Funciones Flecha Y Contexto

TypeScript

Sintaxis

Enums

TypeScript

Sintaxis

Type Aliases Y Aserciones De Tipo

TypeScript

Sintaxis

Clases Y Objetos

TypeScript

Programación Orientada A Objetos

Interfaces Y Su Implementación

TypeScript

Programación Orientada A Objetos

Modificadores De Acceso Y Encapsulación

TypeScript

Programación Orientada A Objetos

Herencia Y Clases Abstractas

TypeScript

Programación Orientada A Objetos

Polimorfismo

TypeScript

Programación Orientada A Objetos

Decoradores Básicos

TypeScript

Programación Orientada A Objetos

Propiedades Y Métodos

TypeScript

Programación Orientada A Objetos

Inmutabilidad

TypeScript

Programación Funcional

Funciones Puras

TypeScript

Programación Funcional

Funciones De Primera Clase

TypeScript

Programación Funcional

Funciones De Alto Orden

TypeScript

Programación Funcional

Conceptos Básicos E Inmutabilidad

TypeScript

Programación Funcional

Funciones De Primera Clase Y Orden Superior

TypeScript

Programación Funcional

Composición De Funciones

TypeScript

Programación Funcional

Métodos Funcionales De Arrays (Map, Filter, Reduce)

TypeScript

Programación Funcional

Tipos Literales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad

TypeScript

Tipos Intermedios Y Avanzados

Unknown, Never Y Tipos Especiales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Mapped

TypeScript

Tipos Intermedios Y Avanzados

Genéricos Con Clases E Interfaces

TypeScript

Tipos Intermedios Y Avanzados

Módulos

TypeScript

Namespaces Y Módulos

Namespaces

TypeScript

Namespaces Y Módulos

Resolución De Módulos

TypeScript

Namespaces Y Módulos

Exportación E Importación De Módulos

TypeScript

Namespaces Y Módulos

Introducción A Módulos

TypeScript

Namespaces Y Módulos

Testing Unitario En Typescript

TypeScript

Testing

Accede GRATIS a TypeScript y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la sintaxis básica y funcionamiento de los mapped types en TypeScript.
  • Aplicar modificadores de propiedades como opcionalidad y solo lectura en mapped types.
  • Utilizar key remapping para transformar nombres de propiedades en tipos derivados.
  • Crear y manipular template literal types para definir tipos de cadenas complejos y expresivos.
  • Integrar mapped types y template literal types para construir tipos reutilizables y seguros en aplicaciones reales.