Template literal types#
En el nivel 2 aprendiste los template literals de JavaScript: strings delimitados por acentos graves que permiten interpolar expresiones con ${}. TypeScript tiene una versión equivalente que opera a nivel de tipos, no de valores: los template literal types.
// En JS, un template literal produce un string en runtime:
const campo = `dato_nombre`;
// En TS, un template literal type produce un TIPO string literal en tiempo de compilacion:
// "nombre" | "rol" pasa a ser "dato_nombre" | "dato_rol"
type Clave = "nombre" | "rol";
type ConPrefijo = `dato_${Clave}`;Cuando se interpola una unión, TypeScript la distribuye: genera un tipo string literal por cada miembro. Dos uniones interpoladas producen el producto cartesiano de sus miembros.
Los helpers intrínsecos de string#
TypeScript incluye cuatro helpers que transforman string literal types. Solo existen a nivel de tipos; no hay función de runtime con esos nombres:
Capitalize<S>— primera letra a mayúscula:"nombre"→"Nombre"Uppercase<S>— todo a mayúsculas:"nombre"→"NOMBRE"Lowercase<S>— todo a minúsculas:"NOMBRE"→"nombre"Uncapitalize<S>— primera letra a minúscula:"Nombre"→"nombre"
El más usado es Capitalize, porque la convención para nombres de método en JavaScript es camelCase: getNombre, setRol, onChange.
Key remapping: la cláusula as#
En el capítulo anterior los mapped types iteraban sobre keyof T y construían el tipo nuevo con las mismas claves. Ahora viene el paso que faltaba: cambiar las claves mientras se itera.
La sintaxis es { [K in keyof T as NuevaClave]: T[K] }. La cláusula as puede ser cualquier tipo string — incluido un template literal type:
// ConPrefixoDato<T> renombra cada clave de T anteponiendo "dato_".
// K & string: keyof T puede incluir symbol o number; la interseccion con string
// le dice al compilador que en este contexto las claves son string literals.
type ConPrefixoDato<T> = {
[K in keyof T as `dato_${K & string}`]: T[K]
};¿Y qué pasa si no pones K & string? El compilador no puede garantizar que K sea un tipo string válido para interpolar, y marca error. La intersección es la forma de estrechar el tipo dentro del template.
Filtrar claves con as ... never#
La cláusula as puede devolver never. Cuando lo hace, TypeScript descarta silenciosamente esa clave del tipo resultado: la propiedad desaparece por completo.
¿Por qué no da error? Porque never en TypeScript es el tipo “sin valor posible”: una propiedad cuya clave es never es imposible de nombrar y, por tanto, no existe. El compilador lo elimina antes de construir el tipo final.
// SoloStrings<T>: conserva solo las propiedades cuyo tipo sea string.
// Si T[K] extends string, devuelve K (se conserva la clave).
// Si no, devuelve never (la clave desaparece).
type SoloStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
};Esto es un filtrado automático: no hace falta listar las claves a mano. El propio tipo decide qué conservar según una condición sobre el valor.
Combinar todo: el patrón Getters<T>#
Ahora que tienes template literal types, Capitalize y key remapping, puedes construir el patrón más representativo: un tipo que, dado el shape de Heroe, genera toda una API de getters tipada sin escribir nada a mano.
// Getters<T>: por cada clave K de T genera una funcion getter tipada.
// La nueva clave es `get${Capitalize<K & string>}`: "nombre" -> "getNombre".
// El tipo del valor es () => T[K]: funcion que devuelve el tipo de la propiedad.
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
};¿Y qué? La respuesta está en lo que ocurre cuando Heroe crece. Si añades una propiedad nivel: number, Getters<Heroe> incluirá automáticamente getNivel: () => number sin tocar nada más. Los nombres de los métodos se derivan del tipo, no se mantienen a mano.
Es el mismo mecanismo que usan librerías como Zustand (para los selectores del store), Vue Pinia (para las acciones de los composables) o los event emitters tipados (onClick, onChange). Aprender a leer este patrón en el código fuente de una librería ya no es intimidante.
Comprueba lo que sabes#
Pregunta 1 de 4
¿Qué produce `type ConPrefijo = `dato_${"nombre" | "rol"}`?`
Tu turno#
Completa los tres tipos del Team Builder hasta que los tests Expect<Equal<...>> queden sin subrayado rojo. Empiezas renombrando claves con un prefijo, sigues generando getters con Capitalize y cierras filtrando propiedades por tipo de valor. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.
Ejercicio · en esta página
Key remapping del Team Builder
Trabajas a nivel de tipos: completa los tres tipos hasta que los tests Expect<Equal<...>> queden sin subrayado rojo. Empiezas renombrando claves con un prefijo, sigues generando getters con Capitalize y cierras filtrando por tipo de valor. Cuando lo tengas, los tres tests pasan de rojo a verde.
Paso 1: Que renombre claves
- RenombrarCon<T, Prefijo> usa la cláusula as con un template literal type para prefijar cada clave.
- K & string garantiza que TypeScript trate la clave como string literal dentro del template.
- El test _T1 queda en verde.
Paso 2: Que genere getters
- GettersDe<T> combina as con `get${Capitalize<K & string>}` para convertir "nombre" en "getNombre".
- Entiendes que Capitalize opera solo a nivel de tipos: no existe en runtime.
- El test _T2 queda en verde.
Paso 3: Que filtre por tipo de valor
- SoloStrings<T> usa as T[K] extends string ? K : never para descartar las claves que no son string.
- Entiendes que mapear una clave a never la elimina del tipo resultado de forma silenciosa.
- El test _T3 queda en verde.
- Reconoces que este patrón es un Pick automático: el tipo decide qué conservar según una condición sobre el valor, sin listar las claves a mano.
Ver soluciones
// Helper de testing de tipos (lo viste en conditional types).
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
// RenombrarCon<T, Prefijo>: recorre las claves de T con un mapped type y,
// dentro de la cláusula as, usa un template literal type para construir
// la nueva clave: `${Prefijo}_${K & string}`.
// K & string es necesario porque keyof T puede incluir symbol o number;
// la intersección con string fuerza a TypeScript a tratarlo como string literal.
type RenombrarCon<T, Prefijo extends string> = {
[K in keyof T as `${Prefijo}_${K & string}`]: T[K]
};
// GettersDe y SoloStrings no son necesarios para este nivel.
type GettersDe<T> = unknown;
type SoloStrings<T> = unknown;
// El primer test pasa: RenombrarCon prefija cada clave con "dato_".
// GettersDe y SoloStrings siguen como unknown, así que _T2 y _T3 aún son rojos.
// Eso está bien: en cada nivel se verde solo lo que se ha resuelto.
type _T1 = Expect<Equal<
RenombrarCon<Heroe, "dato">,
{ dato_nombre: string; dato_rol: string; dato_partidas: number; dato_victorias: number }
>>;
console.log("RenombrarCon correcto: la clave cambia en tiempo de compilacion, el valor permanece igual."); Por qué este nivel
- RenombrarCon usa la cláusula as con un template literal type: `dato_${K & string}`. La intersección K & string es imprescindible porque keyof T puede incluir symbol o number, y un template literal solo acepta string.
- El tipo del valor T[K] no cambia: key remapping solo afecta al nombre de la clave, no a lo que contiene.
- El siguiente nivel añade Capitalize para generar nombres con estilo "getter" en vez de un prefijo plano.
// Helper de testing de tipos (lo viste en conditional types).
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
// RenombrarCon<T, Prefijo>: prefija cada clave con el string dado.
type RenombrarCon<T, Prefijo extends string> = {
[K in keyof T as `${Prefijo}_${K & string}`]: T[K]
};
// GettersDe<T>: combina key remapping con Capitalize para generar "getX".
// Capitalize<S> es un helper intrínseco de TypeScript: pone en mayúscula
// la primera letra del string literal S.
// La clave nueva es `get${Capitalize<K & string>}`: "nombre" -> "getNombre".
// El valor T[K] sigue siendo el tipo de la propiedad original.
type GettersDe<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: T[K]
};
// SoloStrings aún no implementado en este nivel.
type SoloStrings<T> = unknown;
// Los dos primeros tests pasan.
// Salto clave respecto a ok: Capitalize es un helper intrínseco del compilador
// que actúa solo sobre tipos, no sobre valores — no existe en runtime.
type _T1 = Expect<Equal<
RenombrarCon<Heroe, "dato">,
{ dato_nombre: string; dato_rol: string; dato_partidas: number; dato_victorias: number }
>>;
type _T2 = Expect<Equal<
GettersDe<Heroe>,
{ getNombre: string; getRol: string; getPartidas: number; getVictorias: number }
>>;
console.log("GettersDe correcto: Capitalize en el tipo convierte 'nombre' en 'Nombre'."); Por qué es mejor que el anterior
- GettersDe combina la cláusula as con Capitalize: `get${Capitalize<K & string>}` convierte "nombre" en "getNombre". Capitalize es un helper intrínseco del compilador — no existe en runtime.
- El valor sigue siendo T[K]: la función getter no aparece aún en el tipo. Si el ejercicio pidiera () => T[K], cambiaría el tipo del valor, no la clave.
- El salto a excelente introduce el filtrado: cuando as devuelve never, la clave desaparece del tipo resultado.
// Helper de testing de tipos (lo viste en conditional types).
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
// RenombrarCon<T, Prefijo>: prefija cada clave con el string dado.
type RenombrarCon<T, Prefijo extends string> = {
[K in keyof T as `${Prefijo}_${K & string}`]: T[K]
};
// GettersDe<T>: genera "getNombre", "getRol", etc. con Capitalize.
type GettersDe<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: T[K]
};
// SoloStrings<T>: filtra las claves cuyos valores NO son string, mapeándolas a never.
// La cláusula as puede devolver never: cuando lo hace, TypeScript descarta esa clave
// del tipo resultante — la propiedad desaparece por completo.
// T[K] extends string ? K : never: si el tipo del valor es string, se conserva la clave K;
// si no lo es (number, boolean...), se mapea a never y se elimina.
type SoloStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
};
// La clave de excelente: mapear a never NO es un error — es el mecanismo de filtrado.
// Una clave never se descarta silenciosamente. Es el mismo mecanismo por el que
// "Apoyo" desaparecía de la unión en conditional types distribuidos.
// En mapped types eso permite construir algo equivalente a Pick pero sin listar las
// claves a mano: el propio tipo decide cuáles conservar según una condición.
// Los tres tests pasan:
type _T1 = Expect<Equal<
RenombrarCon<Heroe, "dato">,
{ dato_nombre: string; dato_rol: string; dato_partidas: number; dato_victorias: number }
>>;
type _T2 = Expect<Equal<
GettersDe<Heroe>,
{ getNombre: string; getRol: string; getPartidas: number; getVictorias: number }
>>;
type _T3 = Expect<Equal<
SoloStrings<Heroe>,
{ nombre: string; rol: string }
>>;
console.log("Los tres tests en verde: key remapping y template literal types dominados."); Por qué es mejor que el anterior
- SoloStrings usa as con un conditional type: T[K] extends string ? K : never. Si el valor es string, se conserva la clave; si no, se mapea a never y desaparece.
- Mapear a never no es un error: es el mecanismo de filtrado de TypeScript. La clave simplemente no existe en el tipo resultado.
- La lección de fondo: key remapping y template literal types juntos permiten derivar toda una API tipada (nombres de métodos, eventos, selectores) a partir del shape de un tipo de datos, sin escribir nada a mano. Es lo que hacen por dentro librerías como Zustand, Pinia o los event emitters tipados.