JS
() =>
JavaScript · Template Literals · Sicherheit · DSL
Tagged Template Literals in JavaScript
Tag-Funktionen, SQL-Schutz und eigene DSLs

Tagged Template Literals sind weit mehr als hübsche String-Interpolation. Mit einer Tag-Funktion davor verwandeln sie sich in ein mächtiges Werkzeug: SQL-Injection-Schutz, sicheres HTML-Escaping und domänenspezifische Sprachen direkt in JavaScript — ohne externe Parser.

12 Min. Lesezeit Tag-Funktion · String.raw · SQL · HTML · CSS-in-JS ES2015+ · Node.js · Browser

1. Was Tagged Template Literals von normalen Template Strings unterscheidet

Ein normaler Template String wie `Hallo ${name}` interpoliert Ausdrücke direkt in den resultierenden String — JavaScript übernimmt die Verkettung automatisch und gibt immer einen String zurück. Ein Tagged Template Literal funktioniert grundlegend anders: Statt die Interpolation selbst durchzuführen, ruft die JavaScript-Engine eine sogenannte Tag-Funktion auf und übergibt ihr die Einzelteile des Template Strings — die statischen Zeichenketten und die interpolierten Werte getrennt voneinander. Die Tag-Funktion entscheidet dann selbst, was sie damit macht und welchen Wert sie zurückgibt. Das Ergebnis muss kein String sein; es kann ein Array, ein Objekt, ein DOM-Element oder eine Promise sein.

Diese Unterscheidung ist der Kern der ganzen Mächtigkeit von Tagged Template Literals. Sobald eine Tag-Funktion vor dem Backtick steht, verliert JavaScript die Kontrolle über die Interpolation. Das ermöglicht das Escaping von Nutzereingaben bevor sie in einen String einfließen — genau das, was normaler String-Interpolation fehlt und was bei SQL-Abfragen oder HTML-Ausgaben zu Sicherheitsproblemen führt. In realen Projekten findet man Tagged Template Literals überall dort, wo Strings domänenspezifische Regeln haben: in SQL-Bibliotheken wie `sql-template-strings`, in CSS-in-JS-Bibliotheken wie styled-components und in GraphQL-Clients wie Apollo.

2. Anatomie einer Tag-Funktion: strings, values und raw

Eine Tag-Funktion hat eine festgelegte Signatur: Das erste Argument ist ein Array der statischen String-Teile, alle weiteren Argumente sind die interpolierten Werte. Bei tag`Hallo ${name}, du bist ${age} Jahre alt` erhält die Tag-Funktion strings = ["Hallo ", ", du bist ", " Jahre alt"] und values = [name, age]. Das strings-Array hat immer genau einen Eintrag mehr als die values — das erste strings-Element kommt vor dem ersten Ausdruck, das letzte nach dem letzten Ausdruck. Bei einem Template ohne Interpolation hat strings genau einen Eintrag und values ist leer.

Die strings-Array-Elemente sind eingefroren: Sie werden einmalig zur Parse-Zeit erstellt und danach gecacht — dasselbe Tagged Template Literal an derselben Code-Stelle im Quelltext erzeugt bei jedem Aufruf dasselbe strings-Array-Objekt. Das ist wichtig für Performance-kritische Szenarien, weil eine Tag-Funktion das strings-Objekt als Cache-Key verwenden kann. Zusätzlich hat das strings-Array eine raw-Property, die die ursprünglichen Escape-Sequenzen enthält — bei `\n` liefert strings[0] einen tatsächlichen Zeilenumbruch, während strings.raw[0] den String "\\n" mit zwei Zeichen zurückgibt. Dieses raw-Property ist die Grundlage von String.raw.


// Tag function anatomy — receives strings array and spread values
function inspect(strings, ...values) {
  console.log("Static parts:", strings);
  console.log("Raw parts:", strings.raw);
  console.log("Interpolated values:", values);

  // Reconstruct: strings always has one more element than values
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] !== undefined ? `[${typeof values[i]}: ${values[i]}]` : "");
  }, "");
}

const name = "Alice";
const age = 30;
const output = inspect`Hello ${name}, you are ${age} years old`;
// Static parts: ["Hello ", ", you are ", " years old"]
// Interpolated values: ["Alice", 30]
// Output: "Hello [string: Alice], you are [number: 30] years old"

// strings array is frozen and cached — same object reference on repeated calls
function sameRef(strings) { return strings; }
const ref1 = sameRef`test ${"a"}`;
const ref2 = sameRef`test ${"b"}`;
console.log(ref1 === ref2); // true — same call site, same strings object

3. String.raw: der eingebaute Tag für Escape-freie Strings

String.raw ist die einzige in JavaScript eingebaute Tag-Funktion. Sie gibt die rohen Zeichenketten ohne Verarbeitung der Escape-Sequenzen zurück. Das klassische Anwendungsfeld sind Windows-Pfade und reguläre Ausdrücke, wo Backslashes eigentlich Literale sein sollen, nicht Escape-Einleitungen. Statt "C:\\Users\\Alice\\Documents" zu schreiben, funktioniert String.raw`C:\Users\Alice\Documents` — der Backslash wird exakt so übergeben, wie er im Quelltext steht, weil String.raw auf strings.raw zugreift statt auf strings.

Die Implementierung von String.raw ist ein ausgezeichnetes Lernbeispiel, weil sie zeigt, wie einfach eine Tag-Funktion aufgebaut ist: Sie iteriert über strings.raw und verkettet die rohen Strings mit den interpolierten Werten. Für eigene Tag-Funktionen, die Escape-Sequenzen unverändert lassen wollen — etwa für LaTeX-Ausgabe oder Regex-Patterns — ist dieselbe Technik anwendbar. Für den Umgekehrten Fall, wo Escape-Sequenzen im Template aktiv verarbeitet werden sollen, aber Nutzerwerte escaped werden müssen, kombiniert man strings (verarbeitet) mit escaping der values.

4. Sicheres HTML-Escaping mit Tagged Template Literals

Eine der häufigsten Sicherheitslücken in Web-Anwendungen entsteht, wenn Nutzereingaben ohne Escaping direkt in HTML-Strings eingefügt werden. Normale Template Strings machen dieses Problem sogar schlimmer als manuelle String-Verkettung, weil die sauber aussehende Syntax den Entwickler in falscher Sicherheit wiegt. Ein Tagged Template Literal mit einer html-Tag-Funktion löst das Problem an der richtigen Stelle: Die Tag-Funktion escapt automatisch jeden interpolierten Wert, während die statischen Template-Teile unverändert bleiben — denn die hat der Entwickler selbst geschrieben und kontrolliert.

Das Muster ist in der Praxis besonders wertvoll, weil es denselben Template-Syntax-Komfort behält, den Entwickler kennen, aber Cross-Site-Scripting-Angriffe durch dynamische Werte verhindert. Eine sichere html-Tag-Funktion muss die fünf kritischen HTML-Zeichen escapen: &, <, >, " und '. Dabei ist die Reihenfolge wichtig: & muss zuerst ersetzt werden, sonst werden die bereits eingefügten Escape-Sequenzen doppelt escaped. Für komplexere Anwendungsfälle — etwa das gezielte Erlauben bestimmter HTML-Tags — kann die Tag-Funktion einen Sanitizer wie DOMPurify aufrufen, bevor sie den Wert in den String einfügt.


// Safe HTML tagged template literal — escapes all interpolated values
function html(strings, ...values) {
  const escape = (val) => {
    if (val === null || val === undefined) return "";
    return String(val)
      .replace(/&/g, "&")    // must be first!
      .replace(/</g, "<")
      .replace(/>/g, ">")
      .replace(/"/g, """)
      .replace(/'/g, "'");
  };

  return strings.reduce((result, str, i) => {
    const value = values[i] !== undefined ? escape(values[i]) : "";
    return result + str + value;
  }, "");
}

// Usage: user input is automatically escaped
const userInput = '<script>alert("XSS")</script>';
const username = "Alice & Bob";

const safeHtml = html`
  <div class="profile">
    <h1>${username}</h1>
    <p>Bio: ${userInput}</p>
  </div>
`;
// <h1>Alice & Bob</h1>
// <p>Bio: <script>alert("XSS")</script></p>

// Mark trusted HTML to bypass escaping (explicit opt-in)
class SafeHtml {
  constructor(str) { this.value = str; }
}
const trusted = (str) => new SafeHtml(str);

function htmlWithTrust(strings, ...values) {
  const escape = (val) => val instanceof SafeHtml ? val.value : htmlEscape(val);
  return strings.reduce((r, s, i) => r + s + (values[i] !== undefined ? escape(values[i]) : ""), "");
}

5. SQL-Injection verhindern: parametrisierte Queries als Tag

SQL-Injection gehört seit Jahren zu den häufigsten kritischen Sicherheitslücken in Web-Anwendungen — und entsteht fast immer durch manuelle String-Verkettung von SQL-Statements mit Nutzereingaben. Parametrisierte Queries sind die korrekte Lösung, aber die Syntax ist umständlich: Parameter müssen separat als Array übergeben und im Query-String mit Platzhaltern wie $1, $2 markiert werden. Ein Tagged Template Literal vereint die Lesbarkeit von String-Interpolation mit der Sicherheit von parametrisierten Queries: Die Tag-Funktion extrahiert die interpolierten Werte als Parameter-Array und baut den Query-String mit korrekten Platzhaltern auf.

Das Ergebnis ist ein Objekt mit text (der SQL-String mit Platzhaltern) und values (dem Parameter-Array), das direkt an den Datenbankadapter übergeben werden kann. Bibliotheken wie sql-template-strings für Node.js oder postgres (den nativen PostgreSQL-Client) implementieren genau dieses Muster. Wichtig dabei: Die Tag-Funktion muss die statischen Template-Teile ohne Escaping übernehmen — sie stammen vom Entwickler und enthalten den SQL-Skelettcode. Nur die interpolierten Werte werden als Parameter ausgelagert und nie direkt in den SQL-String eingebettet. Das macht SQL-Injection strukturell unmöglich, weil der Datenbankserver die Trennung zwischen Code und Daten selbst durchsetzt.


// SQL tagged template literal — produces parameterized query objects
function sql(strings, ...values) {
  let text = "";
  const params = [];

  strings.forEach((str, i) => {
    text += str;
    if (i < values.length) {
      params.push(values[i]);
      text += `$${params.length}`; // PostgreSQL placeholder
    }
  });

  return { text, values: params };
}

// Usage: looks like interpolation, generates safe parameterized query
const userId = req.params.id;         // untrusted user input
const status = "active";

const query = sql`
  SELECT id, name, email
  FROM users
  WHERE id = ${userId}
    AND status = ${status}
  ORDER BY created_at DESC
`;

// query.text  = "SELECT ... WHERE id = $1 AND status = $2 ..."
// query.values = [userId, "active"]

// Pass directly to pg client — database enforces code/data separation
const result = await db.query(query.text, query.values);

// Composable: build queries from sub-queries
const filter = sql`AND role = ${"admin"}`;
const fullQuery = sql`SELECT * FROM users WHERE active = ${true} ${filter}`;

6. CSS-in-JS und Styled Components: wie es unter der Haube funktioniert

Styled Components ist die bekannteste Bibliothek, die auf Tagged Template Literals aufbaut — und versteht man das Prinzip, erschließt sich das API sofort. Statt eine Klasse zu schreiben und separat mit einem Element zu verbinden, beschreibt man das Styling direkt am Komponenten-Aufruf: styled.button`background: ${props => props.primary ? "#ca8a04" : "white"}`. Die Tag-Funktion erhält hier keine simplen Werte, sondern Funktionen als interpolierte Werte — Funktionen, die später mit den Props der Komponente aufgerufen werden. Die Tag-Funktion generiert eine einmalige CSS-Klasse, injiziert das CSS in den <style>-Block des Dokuments und gibt eine React-Komponente zurück, die diese Klasse trägt.

Dieses Muster zeigt, dass Tagged Template Literals nicht nur Strings transformieren — sie können beliebige Berechnungen und Seiteneffekte auslösen. CSS-in-JS ist ein perfektes Beispiel, weil die Tag-Funktion das gesamte Styling-System kapselt: Hashing der Klassen, Deduplizierung, SSR-Unterstützung und Theme-Interpolation. Für eigene CSS-in-JS-Implementierungen reicht ein einfaches Grundgerüst: Template parsen, CSS-Klasse generieren, in ein <style>-Element injizieren, Element mit der Klasse versehen. Für Produktionseinsatz übernehmen Bibliotheken wie emotion oder linaria diesen Part effizienter.

7. GraphQL-Queries als Tagged Template Literals

Apollo Client und andere GraphQL-Clients nutzen Tagged Template Literals mit dem gql-Tag, um GraphQL-Query-Strings zur Laufzeit in Abstract Syntax Trees zu parsen. Der Vorteil gegenüber gewöhnlichen Strings ist nicht nur Syntaxhervorhebung in Editoren — die Tag-Funktion kann dasselbe Query-Dokument bei wiederholtem Aufruf aus dem Cache zurückgeben, weil das strings-Array gecacht wird. So wird ein GraphQL-Query-String nur einmalig geparst, egal wie oft die Komponente rendert.

Das Caching-Verhalten des strings-Arrays macht Tagged Template Literals besonders wertvoll für teure Parse-Operationen. Wer eigene Mini-DSLs mit Non-trivial-Parsing implementiert — etwa für Abfragesprachen, Formatierungsregeln oder Routing-Patterns — sollte dieses Verhalten ausnutzen: Beim ersten Aufruf das strings-Array als Map-Key verwenden und das Parsergebnis cachen; beim nächsten Aufruf direkt aus dem Cache zurückgeben und nur die values einsetzen. Das reduziert den Overhead des Tagged Template Literal auf die Kosten eines Map-Lookups bei Folgeaufrufen.


// gql-style tag with parse caching — expensive parse only once per call site
const parseCache = new WeakMap();

function gql(strings, ...values) {
  // strings is the same object reference for the same call site
  if (parseCache.has(strings)) {
    const cached = parseCache.get(strings);
    return applyValues(cached, values); // fast path
  }

  // Reconstruct the full query string for parsing
  const queryString = strings.reduce((acc, str, i) => {
    return acc + str + (values[i] !== undefined ? `$var${i}` : "");
  }, "");

  const parsed = parseGraphQL(queryString); // expensive — runs once per call site
  parseCache.set(strings, parsed);
  return applyValues(parsed, values);
}

// Usage with Apollo-style API
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

// Fragment composition via interpolation
const USER_FIELDS = gql`
  fragment UserFields on User {
    id
    name
    email
  }
`;

const GET_USERS = gql`
  query GetUsers {
    users {
      ...UserFields
    }
  }
  ${USER_FIELDS}
`;

8. Tagged vs. untagged Template Literals im direkten Vergleich

Der Vergleich zwischen normalen und Tagged Template Literals zeigt deutlich, wo welches Werkzeug passt. Normale Template Strings sind ideal für einfache Interpolation ohne Sicherheitsanforderungen — Logging, Debug-Ausgaben, interne Strings. Tagged Template Literals sind notwendig, sobald interpolierte Werte aus externen Quellen stammen oder spezielle Semantik haben.

Szenario Untagged (unsicher/ungeeignet) Tagged (korrekt) Begründung
SQL mit Nutzerdaten `SELECT * WHERE id=${id}` sql`SELECT * WHERE id=${id}` SQL-Injection verhindert
HTML mit Nutzereingabe `<p>${userBio}</p>` html`<p>${userBio}</p>` XSS-Schutz automatisch
Windows-Pfade `C:\\Users\\Alice` String.raw`C:\Users\Alice` Kein doppeltes Escaping
GraphQL-Query apollo.query({query: string}) gql`query { ... }` Parse-Caching, Editor-Support
CSS mit Props className={style(props)} styled.div`color:${p=>p.c}` Props-Interpolation, Dedup

Das wichtigste Entscheidungskriterium: Wenn die interpolierten Werte aus nicht vertrauenswürdigen Quellen stammen oder wenn das Ergebnis in einem Kontext mit eigener Escape-Semantik landet (SQL, HTML, Shell-Befehle), ist eine Tag-Funktion nicht optional, sondern Pflicht. Die Tag-Funktion ist der einzige Ort, an dem Escaping konsequent und nicht versehentlich umgehbar ist — im Gegensatz zu manuellen escape()-Aufrufen, die Entwickler schlicht vergessen können.

9. Typische Fehler und Missverständnisse

Der häufigste Fehler beim Einsatz von Tagged Template Literals ist die Annahme, dass die Tag-Funktion immer einen String zurückgeben muss. Das ist falsch — sie kann jeden Typ zurückgeben. Wer das nicht versteht, versucht das Ergebnis wie einen String zu behandeln und wundert sich über Type-Fehler. Ein zweiter verbreiteter Fehler: Man übergibt Arrays oder Objekte als interpolierte Werte und verlässt sich darauf, dass die Tag-Funktion sie sinnvoll serialisiert. Ohne explizite Behandlung in der Tag-Funktion wird JavaScript toString() aufrufen — bei Arrays kommt Komma-separierter String, bei Objekten [object Object] heraus.

Ein subtilerer Fehler betrifft das strings.raw-Property: Es ist nur auf dem TemplateStringsArray-Objekt vorhanden, das die JavaScript-Engine übergibt. Wer versucht, manuell ein Array mit einer raw-Property zu simulieren und an eine Tag-Funktion zu übergeben, kann Tagged Template Literals nicht direkt aufrufen — die Syntax verlangt den Backtick-Call. Mit String.raw({ raw: ["..."] }) kann man raw-Verhalten jedoch programmatisch nutzen. Das ist nützlich für Tests und für Funktionen, die selbst als Tag-Funktion dienen und intern String.raw aufrufen möchten.

10. Zusammenfassung

Tagged Template Literals sind eines der am häufigsten unterbewerteten Features von modernem JavaScript. Die Syntax — ein Bezeichner direkt vor dem Backtick — öffnet eine vollständige Kontrollübernahme über die String-Interpolation. Das erste Argument der Tag-Funktion enthält die statischen Template-Teile als unveränderliches, gecachtes Array; alle weiteren Argumente sind die interpolierten Werte, die die Tag-Funktion transformieren, validieren oder sicher einbetten kann. Das Ergebnis kann jeder JavaScript-Typ sein, nicht nur ein String.

In der Praxis lösen Tagged Template Literals drei kritische Probleme elegant: Sie verhindern SQL-Injection durch automatische Parametrisierung, sie verhindern XSS durch automatisches HTML-Escaping, und sie ermöglichen domänenspezifische Sprachen direkt in JavaScript ohne externe Parser. Bibliotheken wie styled-components, Apollo Client und sql-template-strings zeigen, wie mächtig dieses Muster im Einsatz ist. Der strings-Array-Cache macht Tag-Funktionen auch für Performance-kritische Parsing-Anwendungen geeignet — der Parse-Aufwand fällt nur einmalig pro Call-Site an.

Mironsoft

JavaScript-Entwicklung, Sicherheits-Reviews und moderne Web-Architekturen

JavaScript-Code, der sicher und wartbar ist?

Wir analysieren bestehende JavaScript-Codebasen auf Sicherheitslücken durch unsichere String-Interpolation und modernisieren SQL-Queries, HTML-Ausgaben und Template-Logik mit bewährten Mustern.

Sicherheits-Review

SQL-Injection und XSS-Lücken durch unsichere Template-Nutzung aufspüren und beheben

Refactoring

Unsichere String-Verkettungen durch typsichere Tagged Template Literals ersetzen

DSL-Entwicklung

Domänenspezifische Tag-Funktionen für Query-Builder, Formatter und Template-Systeme

Tagged Template Literals — Das Wichtigste auf einen Blick

Signatur der Tag-Funktion

Erstes Argument: gecachtes TemplateStringsArray mit raw-Property. Weitere Argumente: interpolierte Werte. Rückgabetyp beliebig — kein String erforderlich.

Sicherheitsanwendungen

SQL: Parametrisierung statt Einbettung. HTML: Escaping vor Einbettung. Shell: Escaping aller Sonderzeichen. Tag-Funktion ist der einzige sichere Ort.

Performance: strings-Cache

strings-Array ist bei gleichem Call-Site dieselbe Objektreferenz. Als WeakMap-Key nutzbar für Parse-Caching in gql, Regex oder DSL-Parsern.

Praxis-Bibliotheken

styled-components: CSS-in-JS. Apollo: gql-Tag. sql-template-strings: SQL-Queries. String.raw: eingebaut, Escape-freie Strings.

11. FAQ: Tagged Template Literals in JavaScript

1Was ist ein Tagged Template Literal?
Ein Template String mit einer vorangestellten Tag-Funktion. Die Engine ruft diese Funktion auf und übergibt statische Teile und interpolierte Werte getrennt. Der Rückgabetyp ist beliebig.
2Muss die Tag-Funktion einen String zurückgeben?
Nein — sie kann jeden Typ zurückgeben. styled-components gibt eine React-Komponente zurück, gql ein AST-Objekt, eine sql-Funktion ein Query-Objekt mit text und values.
3strings vs. strings.raw — Unterschied?
strings: Escape-Sequenzen sind verarbeitet (\n = Zeilenumbruch). strings.raw: Rohe Zeichen wie im Quelltext (\n = zwei Zeichen). String.raw nutzt strings.raw.
4Warum ist das strings-Array gecacht?
Es wird einmalig zur Parse-Zeit erstellt. Alle Aufrufe derselben Code-Stelle teilen das gleiche Objekt — nutzbar als WeakMap-Key für Parse-Caching in gql, Regex oder DSL-Parsern.
5Wie verhindert sql`` SQL-Injection?
Die Tag-Funktion setzt Platzhalter ($1, $2) statt der Werte in den SQL-String und übergibt die Werte separat. Der Datenbankserver trennt Code und Daten — Injection ist strukturell unmöglich.
6Können Tag-Funktionen verschachtelt werden?
Ja. Ein interpolierter Wert kann selbst das Ergebnis eines Tagged Template Literals sein — Apollo nutzt das für Fragment-Komposition in GraphQL-Queries.
7Was passiert bei Arrays als interpolierten Werten?
Ohne spezielle Behandlung ruft JavaScript toString() auf — kommaseparierter String. Tag-Funktion muss Arrays explizit mit join() oder rekursivem Escaping behandeln.
8Wie nutzen Styled Components das Feature?
styled.button ist eine Tag-Funktion. CSS-String-Teile und Props-Funktionen als Werte. Ergebnis: einzigartiger Klassenname, injiziertes CSS, React-Komponente mit dieser Klasse.
9String.raw programmatisch ohne Backtick-Syntax?
String.raw({ raw: ['C:\\Users\\Alice'] }) — das raw-Property des ersten Arguments wird genutzt. Nützlich für Tests und dynamisch generierte Strings ohne Backtick-Syntax.
10Wann lieber KEINE Tagged Template Literals?
Bei einfacher Interpolation ohne Sicherheits- oder Semantikanforderungen sind normale Template Strings direkter. Tagged Templates lohnen sich ab dem Punkt, wo Escaping, Parsing oder besondere Rückgabewerte nötig sind.