learning-front

Extra · JavaScript a fondo (opcional)

Tagged template literals

La forma "función" de los template literals: una función que recibe los trozos de texto y los valores por separado. El mecanismo detrás de styled-components y de gql/sql.

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:

javascript
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.

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

javascript
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:

javascript
// 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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

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: &lt;b&gt;inyectado&lt;/b&gt;'
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.

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

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

javascript
// 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).
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.