Los template literals del nivel 2 —las comillas invertidas con ${...}— tienen una forma avanzada
que parece sintaxis mágica de algunas librerías y no lo es: las tagged templates. Consisten en
poner una función justo delante de un template literal. En vez de montar el string, esa función
recibe los trozos de texto y los valores interpolados por separado, y decide qué construir.
Es, literalmente, el mecanismo detrás de styled-components, gql y sql.
Seguimos con el Team Builder. El hilo es pintar fichas de héroe con datos que pone el usuario (un apodo): el caso perfecto para una etiqueta que escape el HTML y evite inyecciones.
Recordatorio: los template literals#
Del nivel 2: un template literal se escribe entre comillas invertidas y permite interpolar valores
con ${...}. El resultado es un string normal:
const nombre = "Tracer";
const rol = "Daño";
// interpola y devuelve un string
const linea = `Héroe ${nombre} de rol ${rol}`;
// linea -> 'Héroe Tracer de rol Daño'Hasta aquí, nada nuevo. Lo nuevo es lo que pasa si pones una función delante de ese template.
Una etiqueta delante: qué recibe la función#
Cuando pones una función pegada a un template literal —la sintaxis etiqueta seguida de comillas
invertidas, sin paréntesis—, JavaScript no monta el string y se lo pasa. Llama a etiqueta
partiendo el template en dos: los trozos fijos de texto en un array, y los valores
interpolados como argumentos sueltos.
// La función recibe (strings, ...valores):
// strings -> array con los trozos FIJOS de texto
// valores -> los valores interpolados con ${...}, en orden
function inspeccionar(strings, ...valores) {
// ['Héroe ', ' de rol ', '']
console.log(strings);
// ['Tracer', 'Daño']
console.log(valores);
return "lo que esta función decida devolver";
}
const nombre = "Tracer";
const rol = "Daño";
// ¡sin paréntesis ni comas!
inspeccionar`Héroe ${nombre} de rol ${rol}`;Dos detalles importantes: strings siempre tiene un elemento más que valores (los valores van en
los huecos entre los trozos, así que sobra un trozo al final, aquí el ''). Y la función puede
devolver lo que quiera: un string, un objeto, un componente. No está obligada a reconstruir el
texto.
Tu propia etiqueta: resaltar#
Construyamos una. resaltar reconstruye el texto, pero envolviendo cada valor entre << y >>. La
clave es recorrer los trozos e ir intercalando cada uno con su valor:
function resaltar(strings, ...valores) {
// reduce arrastra el texto montado: por cada trozo, pega el trozo y, si queda
// un valor para ese hueco, lo pega resaltado detrás.
return strings.reduce((salida, trozo, i) => {
// el último trozo no tiene valor
const marca = i < valores.length ? "<<" + valores[i] + ">>" : "";
return salida + trozo + marca;
}, "");
}
const nombre = "Tracer";
const rol = "Daño";
// 'Pick: <<Tracer>> (<<Daño>>)'
console.log(resaltar`Pick: ${nombre} (${rol})`);Lo interesante no es el <<>>, es el patrón: como la etiqueta ve los valores por separado de
los trozos fijos, puede hacerles algo a cada uno antes de pegarlos. Resaltarlos, formatearlos… o
escaparlos.
Una etiqueta útil: seguro (escapar HTML)#
El caso real. Si construyes HTML metiendo datos del usuario sin tratar, te la juegas: un apodo como
<img src=x onerror="..."> o <script>...</script> se ejecutaría al insertarlo en la página. Eso
es un XSS (cross-site scripting). Una etiqueta que escapa los valores lo neutraliza, porque
solo toca lo interpolado —los datos—, no la plantilla:
// Escapa los caracteres que el navegador interpretaría como HTML.
function escapar(v) {
return String(v)
// el & primero, para no re-escapar los siguientes
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
function seguro(strings, ...valores) {
return strings.reduce((salida, trozo, i) => {
// SOLO se escapan los valores
const valor = i < valores.length ? escapar(valores[i]) : "";
return salida + trozo + valor;
}, "");
}
// imagina que esto viene de un input de usuario
const apodo = "<b>inyectado</b>";
// 'Apodo: <b>inyectado</b>'
console.log(seguro`Apodo: ${apodo}`);
// El <b> se convierte en texto: se mostraría literal, no como etiqueta. XSS evitado.Fíjate en por qué la separación importa: la plantilla (los trozos fijos) es tuya y es de fiar; los valores vienen de fuera y no lo son. La tagged template te da justo esa frontera, así que escapas lo de fuera sin tocar lo de dentro.
Para qué se usa de verdad#
Quita la sensación de truco: las herramientas que ya has oído nombrar son tagged templates.
// styled-components: styled.div es una FUNCIÓN, y el template literal es su tag.
const Boton = styled.div`
color: red;
padding: 8px;
`;
// styled.div recibe los trozos de CSS y los valores, y devuelve un componente.
// gql (GraphQL) y sql: lo mismo, funciones delante de un template.
// Son funciones importadas de sus respectivas librerías (graphql-tag, sql-template-strings...);
// el mecanismo es exactamente el que acabas de aprender.
const QUERY = gql`
query {
heroes {
nombre
}
}
`;No hay sintaxis nueva en esas librerías: es esta misma del lenguaje. styled.div, gql y sql son
funciones que reciben los trozos y los valores de un template y construyen algo —un componente con
estilos, una consulta—. Ahora sabes el mecanismo exacto.
Y para que veas que no hay magia, aquí tienes una mini-versión del corazón de styled-components,
en diez líneas y ejecutable. La gracia real de styled es que las interpolaciones pueden ser funciones
de las props; nuestra etiqueta las resuelve igual:
// estilo recibe los trozos de CSS y los valores, y devuelve una función: dale las
// props y te monta el CSS final, resolviendo las interpolaciones que sean funciones.
function estilo(strings, ...valores) {
return (props) =>
strings.reduce((css, trozo, i) => {
// el último trozo no tiene valor
const valor = i < valores.length ? valores[i] : "";
// si la interpolación es una función, la llamamos con las props (como hace styled)
const resuelto = typeof valor === "function" ? valor(props) : valor;
return css + trozo + resuelto;
}, "");
}
const botonCss = estilo`
color: ${(p) => (p.primario ? "white" : "black")};
padding: 8px;
`;
// 'color: white; padding: 8px;'
console.log(botonCss({ primario: true }));
// 'color: black; padding: 8px;'
console.log(botonCss({ primario: false }));styled.div hace exactamente esto y, además, mete el CSS resultante en una clase y devuelve un
componente de React. Pero el mecanismo que lo sostiene es el que acabas de escribir: una función delante
de un template.
String.raw#
El lenguaje trae una etiqueta de ejemplo, String.raw, que devuelve el texto sin procesar las
secuencias de escape (como \n o \t). Una etiqueta normal procesa esos escapes; String.raw los
deja literales:
// Un template normal procesa los escapes: \n se convierte en un salto de línea.
// dos líneas
console.log(`linea1\nlinea2`);
// String.raw devuelve el texto crudo: \n queda como barra y n, literal.
// 'C:\nombre\usuario' (las \n y \u no se procesan)
console.log(String.raw`C:\nombre\usuario`);
// Útil para regex: en un string normal, \d hay que escribirlo como \\d.
// Con String.raw la barra es literal; el patrón queda limpio.
const patronDigitos = new RegExp(String.raw`^\d{4}$`);
// true: cuatro dígitos exactos
console.log(patronDigitos.test("2026"));
// false: letras no pasan
console.log(patronDigitos.test("abc"));Es útil para rutas de Windows y para patrones de regex escritos como texto, donde quieres conservar las
barras invertidas tal cual: \d en lugar de \\d.
Pruébalo tú#
En la consola: qué arrays recibe una etiqueta (inspeccionar), una etiqueta que resalta los valores,
otra que los escapa y una mini-versión de styled-components. Cambia el apodo por
<script>alert(1)</script> y mira cómo seguro lo convierte en texto inofensivo en lugar de dejarlo
pasar; cambia primario: true por false y observa cómo estilo resuelve la interpolación-función. Pulsa Ejecutar (o Ctrl+Enter) para ver la consola.
Comprueba lo que sabes#
Pregunta 1 de 5
Si pones una función `etiqueta` delante de un template literal (una tagged template), ¿qué recibe esa función?
Tu turno#
Construye tu propia etiqueta seguro que escape los apodos antes de mostrarlos por la consola, para que un dato del
usuario no inyecte HTML. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en cómo
el nivel Excelente escapa también las comillas para ser seguro dentro de atributos y conecta el truco
con styled-components.
Ejercicio · en esta página
Tu propia mini-etiqueta
Muestra una ficha por héroe en la consola. El apodo lo escribe el usuario, así que es dato no confiable: uno trae HTML dentro. Construye una tagged template `seguro` que separe los trozos fijos de los valores y escape esos valores, de modo que un apodo con <b> (o un <script>) salga como texto escapado y no como HTML inyectable.
Paso 1: Que funcione
- Se muestra una ficha por héroe en la consola con nombre, rol y apodo.
- Vale montar el string con un template literal normal.
- Observas el problema: el apodo de Mercy aparece sin escapar (el <b> está ahí tal cual).
Paso 2: Que esté pulido
- `seguro` es una tagged template real: recibe (strings, ...valores).
- Escapas los valores antes de pegarlos; el apodo malicioso sale como texto.
- Reconstruyes el texto intercalando trozos y valores (p. ej. con reduce).
Paso 3: Que sea excelente
- El escape cubre también las comillas: seguro dentro de atributos, no solo en texto.
- La etiqueta es reutilizable y la usas para todas las fichas.
- Comentas el paralelo con styled-components/gql y que React escapa por ti.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// ficha() es una función normal que monta el HTML con un template literal e
// interpola TODO directamente. Con apodos limpios se ve bien, pero...
// - el apodo lo pone el usuario. El de Mercy es "<b>INYECTADO</b>": si este
// string se metiera en innerHTML, el navegador lo renderizaría como HTML.
// En la consola lo verás tal cual: el <b> y el </b> de texto plano.
// - Cambia ese apodo por "<img src=x onerror=...>" o un "<script>": en la
// consola se ve inocente, pero en un innerHTML real ejecutaría código ajeno.
// Funciona para datos de confianza; con datos del usuario, es una puerta abierta.
// ════════════════════════════════════════════════════════════════════════════
const FICHAS = [
{ nombre: "Tracer", rol: "Daño", apodo: "la veloz" },
{ nombre: "Reinhardt", rol: "Tanque", apodo: "el muro" },
{ nombre: "Mercy", rol: "Apoyo", apodo: "<b>INYECTADO</b>" },
];
// Template literal normal: interpola nombre, rol y apodo SIN tratarlos.
function ficha(h) {
return "Nombre: " + h.nombre + " | Rol: " + h.rol + " | Apodo: " + h.apodo;
}
// Muestra cada ficha en la consola. Mercy muestra el <b> sin escapar.
console.log("--- Fichas (sin escapar) ---");
for (const h of FICHAS) {
// el apodo de Mercy sale como texto con <b>: en un innerHTML se inyectaría
console.log(ficha(h));
} Por qué este nivel
- ficha() es una función normal que interpola todo directamente en un template literal. Con datos limpios se ve bien.
- El fallo: el apodo lo pone el usuario y se mete sin escapar; '<b>INYECTADO</b>' se RENDERIZA como HTML. Con <script>/onerror sería un XSS.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - `seguro` es una TAGGED TEMPLATE: una función puesta delante del template
// literal. Recibe los trozos fijos (strings) y los valores interpolados por
// separado, y ESCAPA cada valor antes de pegarlo.
// - el apodo "<b>INYECTADO</b>" se convierte en "<b>INYECTADO</b>":
// en la consola se ve la entidad HTML en vez del tag bruto. Si este string se
// metiera en un innerHTML, el navegador lo mostraría como texto, no como HTML.
//
// Su límite respecto a Excelente: escapa <, > y &, pero NO las comillas. Si
// interpolaras dentro de un ATRIBUTO (title="${valor}"), un valor con comillas
// podría cerrarlo y meter otros atributos. Un escape de verdad cubre también " y '.
// ════════════════════════════════════════════════════════════════════════════
const FICHAS = [
{ nombre: "Tracer", rol: "Daño", apodo: "la veloz" },
{ nombre: "Reinhardt", rol: "Tanque", apodo: "el muro" },
{ nombre: "Mercy", rol: "Apoyo", apodo: "<b>INYECTADO</b>" },
];
// Escapa los caracteres de HTML… pero solo tres (le faltan las comillas).
function escapar(v) {
return (
String(v)
// el & primero, para no re-escapar los de abajo
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
);
}
// La etiqueta: recibe (strings, ...valores) y reconstruye el texto pegando cada
// trozo con su valor YA escapado. strings tiene un elemento más que valores.
function seguro(strings, ...valores) {
return strings.reduce((salida, trozo, i) => {
// último trozo: sin valor
const valor = i < valores.length ? escapar(valores[i]) : "";
return salida + trozo + valor;
}, "");
}
// Ahora ficha usa la etiqueta seguro: los valores se escapan solos.
function ficha(h) {
return seguro`Nombre: ${h.nombre} | Rol: ${h.rol} | Apodo: ${h.apodo}`;
}
// Muestra cada ficha en la consola. El apodo de Mercy sale escapado.
console.log("--- Fichas (con escape) ---");
for (const h of FICHAS) {
// el apodo de Mercy sale como <b>INYECTADO</b>: texto seguro
console.log(ficha(h));
} Por qué es mejor que el anterior
- seguro es una tagged template: recibe trozos y valores por separado y ESCAPA los valores antes de pegarlos. El <b> del apodo queda como texto.
- Separa lo fijo (la plantilla) de lo variable (los datos), que es justo lo que hace que el escape sea posible y fiable.
- Su límite: escapa < > & pero no las comillas; si interpolaras dentro de un atributo, un valor con comillas podría romperlo.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - El escape cubre los CINCO caracteres peligrosos en HTML, comillas incluidas
// (" y '), así que la etiqueta es segura tanto en contenido de texto como dentro
// de un atributo (title="${...}"). Lo demostramos interpolando el apodo también
// en una posición de atributo.
// - La etiqueta `html` es reutilizable y de propósito general: cualquier plantilla
// de la app puede apoyarse en ella para interpolar datos sin riesgo.
//
// El paralelo con lo que usarás de verdad: `styled.div\`...\``, `gql\`...\`` y
// `sql\`...\`` SON tagged templates. Una función (styled.div, gql, sql) delante de
// un template literal que recibe trozos y valores y construye algo con ellos —un
// componente con estilos, una query—. Ahora sabes el mecanismo exacto. Y ojo: en
// React o Vue el escape del HTML lo hace el framework POR TI por defecto; esto es
// para entender qué pasa debajo y para cuando montas HTML a mano.
// ════════════════════════════════════════════════════════════════════════════
const FICHAS = [
{ nombre: "Tracer", rol: "Daño", apodo: "la veloz" },
{ nombre: "Reinhardt", rol: "Tanque", apodo: "el muro" },
{ nombre: "Mercy", rol: "Apoyo", apodo: "<b>INYECTADO</b>" },
];
// Mapa de escape completo: los cinco caracteres que pueden romper HTML o atributos.
const ESCAPES = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
// Escapa cualquier valor (lo pasa a string primero) sustituyendo esos cinco.
function escapar(v) {
return String(v).replace(/[&<>"']/g, (c) => ESCAPES[c]);
}
// Etiqueta reutilizable: intercala los trozos fijos con los valores escapados.
function html(strings, ...valores) {
return strings.reduce((salida, trozo, i) => {
const valor = i < valores.length ? escapar(valores[i]) : "";
return salida + trozo + valor;
}, "");
}
// La ficha interpola el apodo DOS veces: en texto y simulando un atributo.
// Gracias al escape de comillas, el atributo no se puede romper.
function ficha(h) {
return html`Nombre: ${h.nombre} | Rol: ${h.rol} | Apodo: ${h.apodo} |
title="${h.apodo}"`;
}
// Muestra cada ficha en la consola. El apodo de Mercy sale escapado en ambas posiciones.
console.log("--- Fichas (escape completo) ---");
for (const h of FICHAS) {
// el apodo de Mercy: <b>INYECTADO</b> en texto y en posición de atributo
console.log(ficha(h));
} Por qué es mejor que el anterior
- Escape completo (también comillas): la etiqueta html es segura entre etiquetas Y dentro de un atributo (lo demuestra interpolando en title).
- Etiqueta reutilizable y de propósito general; cualquier plantilla de la app puede apoyarse en ella.
- Comenta el paralelo real: styled.div``, gql``, sql`` son tagged templates; y en React/Vue el escape lo hace el framework por ti.