Template Literals kamen mit ES6 (2015) und haben Konkatenation in modernem Code praktisch ersetzt. Backticks erlauben Multi-Line-Strings ohne \n, ${} interpoliert beliebige Expressions — und Tagged Templates verwandeln das Konstrukt in einen Funktions-Aufruf mit Zugriff auf statische Strings und dynamische Werte. Aus diesem Hook entstehen Mini-DSLs für HTML-Sanitization, SQL-Building, i18n und CSS-in-JS. Dieser Artikel erklärt die Syntax, die String.raw-Variante und die Stelle, an der Tagged Templates nicht automatisch XSS-sicher sind.

Was sind Template Literals?

Ein Template Literal ist ein String, der mit Backticks (`) statt einfachen oder doppelten Anführungszeichen begrenzt ist. Innerhalb dieser Backticks darf der Inhalt mehrere Zeilen umfassen, Quotes ohne Escaping enthalten und über ${expression} beliebige JavaScript-Expressions einbetten. Das Ergebnis ist ein normaler string — bis auf den Sonderfall der Tagged Templates, der ihn in einen Funktions-Aufruf verwandelt.

JavaScript basics.js
const name = 'Mia';
const greeting = `Hallo, ${name}!`;
console.log(greeting); // Hallo, Mia!

// Quotes ohne Escaping
const mixed = `Sie sagte: "Das ist 'okay'."`;
console.log(mixed);

// Backslash-Escape für Backtick
const code = `Nutze \`npm install\``;
console.log(code);

// Backslash-Escape für ${ wenn nicht interpoliert werden soll
const literalDollar = `Preis: \${amount}`;
console.log(literalDollar);
Output
Hallo, Mia!
Sie sagte: "Das ist 'okay'."
Nutze `npm install`
Preis: ${amount}

${} mit beliebigen Expressions

Innerhalb von ${...} darf jede gültige JavaScript-Expression stehen — Function-Calls, Ternary, Property-Access, arithmetische Operationen, sogar weitere Template Literals. Statements sind nicht erlaubt (kein if, kein for), denn die Position ist eine Expression-Position. Der Wert wird mit String(...) in einen String konvertiert, bevor er eingefügt wird.

JavaScript interpolation.js
const a = 5;
const b = 10;

// Arithmetik direkt in ${}
console.log(`Summe: ${a + b}, Produkt: ${a * b}`);

// Function-Call
const upper = (s) => s.toUpperCase();
console.log(`Gross: ${upper('hallo')}`);

// Ternary für Verzweigung
const score = 72;
console.log(`Status: ${score >= 60 ? 'bestanden' : 'durchgefallen'}`);

// Property-Access mit Optional-Chaining
const user = { profile: { name: 'Mia' } };
console.log(`Name: ${user?.profile?.name ?? 'unbekannt'}`);

// String-Konversion: Object -> [object Object]
console.log(`Obj: ${{ a: 1 }}`); // 'Obj: [object Object]'
Output
Summe: 15, Produkt: 50
Gross: HALLO
Status: bestanden
Name: Mia
Obj: [object Object]

Der letzte Punkt ist eine häufige Falle: Wer ein Objekt direkt einfügt, bekommt [object Object]. Lösung: explizites JSON.stringify(...) in der Interpolation.

Multi-Line-Strings ohne \n

Backticks erlauben echte Zeilenumbrüche im Source — sie werden 1:1 in den String übernommen. Das ersetzt die früher übliche 'line 1\n' + 'line 2'-Konkatenation und ist besonders praktisch für Templates, SQL-Strings, CLI-Hilfetexte und JSON-Schemata.

JavaScript multi-line.js
const sql = `
    SELECT id, name, email
    FROM users
    WHERE active = true
    ORDER BY created_at DESC
    LIMIT 10
`;
console.log(sql);

// Indent ist Teil des Strings — manchmal ungewollt
const help = `Usage: cli [options]

Options:
--help     Show help
--version  Print version
`;
console.log(help);
Output

            SELECT id, name, email
            FROM users
            WHERE active = true
            ORDER BY created_at DESC
            LIMIT 10
        
Usage: cli [options]

  Options:
    --help     Show help
    --version  Print version

Wenn die Source-Einrückung nicht im Output-String erscheinen soll, hilft eine Tagged-Template-Funktion wie dedent — sie strippt das gemeinsame Leading-Whitespace aller Zeilen.

Tagged Templates: Funktion + Backticks

Wird vor einem Template Literal ein Identifier ohne Klammern gesetzt, wird das Literal nicht mehr zu einem String konkateniert — sondern als Funktions-Aufruf ausgewertet. Die Funktion bekommt ein Strings-Array mit den statischen Teilen und alle interpolierten Werte als Rest-Argumente. Der Rückgabewert kann beliebig sein: ein String, ein DOM-Element, ein SQL-Statement-Objekt, eine Promise.

JavaScript tagged-template-basis.js
function tag(strings, ...values) {
    console.log('strings:', strings);
    console.log('values: ', values);
    return strings.raw[0]; // demo
}

const name = 'Mia';
const age = 28;
tag`Hallo ${name}, du bist ${age} Jahre alt.`;
Output
strings: [ 'Hallo ', ', du bist ', ' Jahre alt.' ]
values:  [ 'Mia', 28 ]

Die Anzahl der Strings ist immer um 1 grösser als die Anzahl der Values — strings[i] steht vor values[i], der letzte String ist der Tail nach der letzten Interpolation. Das gibt dem Tag volle Kontrolle über die Verarbeitung jeder Substitution.

Tagged Templates in der Praxis

Aus dem Hook entstehen Mini-DSLs. Die drei häufigsten:

HTML-Sanitization — Statische Strings sind vertrauenswürdig (Entwickler-Quelltext), Substitutions werden escaped. Das ist das Sicherheitsmodell von lit-html.

SQL-Builder — Statische Strings sind das Query-Skelett, Substitutions werden zu Parameter-Bindings. Statt SQL-Injection per String-Konkatenation entsteht ein parametrisiertes Statement. Das Pattern liegt postgres.js und slonik zugrunde.

i18n — Der Tag schlägt Schlüssel in einer Translation-Map nach und ersetzt Platzhalter sprachgerecht.

JavaScript tagged-templates-praxis.js
// 1) HTML-Sanitization
const escape = (s) => String(s)
    .replaceAll('&', '&')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;');

function html(strings, ...values) {
    return strings.reduce((acc, str, i) => {
        const v = i < values.length ? escape(values[i]) : '';
        return acc + str + v;
    }, '');
}

const userInput = '<script>alert(1)</script>';
console.log(html`<p>Hallo ${userInput}!</p>`);

// 2) SQL-Builder (vereinfacht)
function sql(strings, ...values) {
    const text = strings.reduce(
        (acc, str, i) => acc + str + (i < values.length ? `$${i + 1}` : ''),
        ''
    );
    return { text, values };
}

const id = 42;
console.log(sql`SELECT * FROM users WHERE id = ${id}`);

// 3) i18n
const dict = { en: { greet: 'Hello' }, de: { greet: 'Hallo' } };
const t = (lang) => (strings, ...values) =>
    strings.reduce((acc, str, i) =>
        acc + str.replace(/\{(\w+)\}/g, (_, k) => dict[lang][k] || k)
            + (values[i] ?? ''), ''
    );

const de = t('de');
console.log(de`{greet}, ${'Mia'}!`);
Output
<p>Hallo &lt;script&gt;alert(1)&lt;/script&gt;!</p>
{ text: 'SELECT * FROM users WHERE id = $1', values: [ 42 ] }
Hallo, Mia!

Raw-Strings via String.raw

String.raw ist ein eingebauter Tag, der Escape-Sequenzen nicht verarbeitet — \n bleibt als zwei Zeichen \ und n erhalten. Das ist nützlich für Pfad-Strings auf Windows, RegExp-Strings und alle Stellen, an denen Backslashes bedeutungstragend sind.

JavaScript string-raw.js
// Normal: \n ist Newline
const a = `line1\nline2`;
console.log(a.length); // 11

// Raw: \n bleibt 2 Zeichen
const b = String.raw`line1\nline2`;
console.log(b.length); // 12
console.log(b);

// Windows-Pfad ohne doppelte Backslashes
const path = String.raw`C:\Users\Mia\Documents`;
console.log(path);

// RegExp-Source als String
const pattern = String.raw`\d{3}-\d{4}`;
const re = new RegExp(pattern);
console.log(re.test('555-1234')); // true

// Innerhalb eines eigenen Tags: strings.raw nutzen
function tag(strings, ...values) {
    return strings.raw.join('');
}
console.log(tag`a\nb`); // 'a\nb' (Backslash-n)
Output
11
12
line1\nline2
C:\Users\Mia\Documents
true
a\nb

Escape-Sequenzen in Template Literals

Template Literals akzeptieren die gleichen Escape-Sequenzen wie normale Strings: \n, \t, \\, ä, \x41, sowie zwei Template-spezifische Escapes — \` (Backtick) und \${ (literales Dollar-Curly, ohne Interpolation).

EscapeBedeutung
\`wörtliches Backtick
\${wörtliches ${ ohne Interpolation
\\wörtlicher Backslash
\n, \tNewline, Tab
äUnicode-Codepoint (4 Hex)
\u{1F600}Unicode-Codepoint (variabel)
\x41Latin-1-Codepoint (2 Hex)

In Tagged Templates mit ungültigen Escapes (z. B. \u ohne folgende Hex-Ziffern) liefert strings[i] undefined, der strings.raw[i]-Eintrag enthält die Roh-Sequenz. Diese Toleranz seit ES2018 erlaubt Tags wie LaTeX-Builder, in denen \u einfach Backslash-u meint.

Best Practices für Tagged Templates

  • Schlanker Tag: Eine Tag-Funktion sollte fokussiert sein — eine Verantwortung pro Tag (Escaping oder SQL-Param-Binding oder i18n), nicht alles in einem.
  • Substitutions sanitisieren, statische Strings vertrauen: Im html-Tag werden nur die values escaped; die strings sind Source-Code und sicher. Wer das umdreht, hat ein Sicherheitsproblem.
  • Strings.raw bewusst nutzen: Für RegExp-, LaTeX-, Markdown-Tags ist raw-Zugriff der Schlüssel — sonst frisst der Parser Escape-Sequenzen vor dem Tag.
  • Performance: Caching nutzen: Das strings-Array wird pro Source-Position frozen und für alle Aufrufe wiederverwendet. Tags können das als WeakMap-Key zum Cachen genutzter Templates verwenden — relevant z. B. für DOM-Templates in lit.
  • Keine Side-Effects in Substitutions: Substitutions werden zur Aufruf-Zeit ausgewertet; aufwändige Berechnungen sollten ausgelagert werden.

Performance: Template vs. Konkatenation

In modernen Engines (V8, JavaScriptCore, SpiderMonkey) sind Template Literals und +-Konkatenation in der Performance praktisch identisch — beides läuft über den gleichen interne String-Concat-Pfad. Der Compiler erkennt einfache Templates ohne Tag und ersetzt sie zur Compile-Time direkt durch Konkatenation. Lesbarkeit ist also der einzige relevante Unterschied — und Lesbarkeit gewinnt fast immer für Templates.

AspektTemplate Literal+-Konkatenation
Lesbarkeithochbei vielen Variablen niedrig
Multi-Linenativ via Backticksumständlich \n + +
Performancegleichgleich
Interpolationjede Expressionnur Variablen
Mini-DSLTagged Templates möglichnicht möglich
Browser-Supportseit 2015seit immer

Häufige Fragen

Funktionieren Template Literals geschachtelt?

Ja, beliebig tief. Innerhalb von ${} darf wieder ein Backtick-String stehen: `outer ${`inner ${val}`}`. Praktisch wird das z. B. in Bedingungs-Strings: `<p class="${active ? `btn ${variant}` : 'btn-default'}">`. Lesbarkeit leidet schnell — bei zwei Schachtel-Ebenen lieber in eine eigene Helper-Funktion auslagern.

Performance-Unterschied zu Konkatenation?

In modernen Engines praktisch identisch. V8 erkennt einfache Templates und ersetzt sie zur Compile-Time durch Konkatenation. Tagged Templates haben einen kleinen Overhead durch den Funktions-Aufruf, aber das strings-Array wird gecacht und wiederverwendet — also nur einmal alloziert pro Source-Position. Lesbarkeit gewinnt in jedem realen Code.

Sind Template Literals immer Strings?

Untagged: ja, immer. Tagged: nein. Ein Tag-Funktion kann beliebige Werte zurückgeben — ein DOM-Element (lit-html), ein Query-Objekt (postgres.js), eine Promise, sogar undefined. Das ist genau der Punkt, der Tagged Templates zu einer DSL-Plattform macht.

Kann ich Newline-Zeichen escapen?

Ja, mit \n wie in normalen Strings. Oder einfach einen echten Zeilenumbruch im Source nutzen — Backticks akzeptieren ihn. Kombinierbar: \n für explizite Newline an einer Stelle, echter Umbruch für Source-Lesbarkeit. Wer das Trailing-Newline am Source-Ende nicht im String haben will, schliesst die Backtick-Klammer auf gleicher Zeile.

Was genau ist String.raw?

Ein eingebauter Tag, der Escape-Sequenzen NICHT verarbeitet. String.rawa\nb ergibt 4 Zeichen (a, \, n, b), nicht 3 (a, Newline, b). Implementiert ist es trivial: (s) => s.raw.join('') — plus Substitutions zwischen den Raw-Teilen. Anwendungsfälle: Windows-Pfade, RegExp-Source, LaTeX-Strings, alles mit bedeutungstragenden Backslashes.

Wie unterscheide ich Tagged Template von Function-Call?

Backtick statt Klammern. fn(hi) ist ein normaler Function-Call mit dem String 'hi' als Argument. fnhi ohne Klammern ist Tagged-Template-Aufruf — die Funktion bekommt strings = ['hi'] und keine Values. Das eine Zeichen — Klammer oder Backtick — kippt die Aufruf-Konvention komplett.

Sind Template Literals XSS-sicher?

Nein, untagged definitiv nicht. `<p>${userInput}</p>` fügt User-Input ungeprüft ein und ist genau die XSS-Lücke, die innerHTML seit Jahren liefert. Tagged Templates wie html`` in lit-html oder eigene Sanitizer-Tags machen sie sicher, indem sie jede Substitution escapen oder als Text-Node setzen. Sicherheits-Architektur lebt im Tag, nicht in der Syntax selbst.

Funktionieren Template Literals in Object-Keys?

Ja, als computed property: { [`key${i}`]: val }. Die Klammern um den Key öffnen Expression-Position, dort ist jeder String-Ausdruck erlaubt — inklusive Template Literal. Praktisch für dynamische Map-Keys, Event-Listener-Maps oder generierte CSS-Custom-Properties.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Syntax & Sprachkern

Zur Übersicht