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ícateSintaxis 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 propiedadP
del tipoT
.keyof T
: Devuelve una unión de todas las claves (nombres de propiedades) del tipoT
.T[P]
: Accede al tipo de la propiedadP
en el tipoT
.
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úsculasLowercase<T>
: Convierte todas las letras a minúsculasCapitalize<T>
: Convierte la primera letra a mayúsculaUncapitalize<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.
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
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 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.