learning-front

Extra · Type-level TypeScript (opcional)

Key remapping y template literal types

Renombrar y filtrar las claves de un mapped type con la cláusula as, y construir nuevos nombres de clave con template literal types y Capitalize.

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.

typescript
// 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:

typescript
// 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.

typescript
// 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.

typescript
// 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.
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.