【TS】今さら聞けないタグ付きテンプレートリテラル


はじめに

いきなりですがJSで以下のようなコードを見たことがないでしょうか?

const data1 = "test";
const data2 = "てすと";

hoge`ほげほげ${data1}ふがふが${data2}`

これは 「タグ付きテンプレートリテラル」 と呼ばれるもので、実態としてはhogeという関数を呼び出しています。
よく見かけるのがReactReactNativestyled-componentsを使っている時だと思います。

今回はこの「タグ付きテンプレートリテラル」について、内部的にどんな挙動をしているかや、どんな時に使うかをTypescriptのサンプルを交えて解説していこうと思います。

テンプレートリテラルとは

タグ付きの話をする前に 「テンプレートリテラル」 についておさらいします。
テンプレートリテラルはES2015で追加された機能で、文字列中に変数を組み込む際に使用します。
通常、シングルクォートやダブルクォートで文字列を扱うかと思いますが、テンプレートリテラルではバッククォートで記載します。

const name: string = 'Zenn太郎';
// バッククォート``で囲い、変数部分は${}で記載する
const text: string = `おはようございます。 ${name} さん`;
console.log(text); // --> おはようございます。 Zenn太郎 さん

タグ付きテンプレートリテラル

MDNの同じページに 「タグ付きテンプレートリテラル」 の記載があります。

以下、引用です。

タグ付きテンプレートは、テンプレートリテラルのより高度な形式です。

タグを使用すると、テンプレートリテラルを関数で解析できます。タグ関数の最初の引数には、文字列リテラルの配列を含みます。残りの引数は式に関連付けられます。

タグ関数は、これらの引数に対して何でも望み通りの操作を実行することができ、加工された文字列を返します。 (または、以下の例の一つで示しているように、まったく異なるものを返すこともできます。)

ポイントは 「テンプレートリテラルを関数で解析」 できるという点です。
通常のテンプレートリテラルは指定されたフォーマットに従って文字列を返すだけですが、解析を行うことで全く別な値を返すこともできます。

呼び出し方は冒頭のコードのように関数名の後にテンプレートリテラルを記載して呼び出します。

tag`hogehoge${fuga}`;

どういう時に使う?

先ほどのテンプレートリテラルの例の場合、nameが空文字だった場合は以下のように表示されてしまいます。

おはようございます。  さん

この内容を画面に表示することを考えた場合、nameが空の場合は「何も表示させない」方が正解のように思えます(もしくは何かしらの警告を表示する等です)。
このように 「テンプレートリテラル中の変数部分を解析して何らかの処理をしたい」 場合にタグ付きテンプレートリテラルを利用します。

タグ付きテンプレートリテラルの書き方

関数として通常通り定義します。
この時、引数として2つの値が与えられます。
1つ目はTemplateStringsArray型の値で、これは 「テンプレートリテラルの固定文字部分」 を区切った配列です。
先の例のおはようございます。 ${name} さんの場合は["おはようございます。 ", " さん"]という2つの要素を持った配列になります。
2つ目はany型の配列で 「テンプレートリテラルの変数部分」 の配列になります。
先の例では["Zenn太郎"]のような値が入ってきます。

TemplateStringsArrayはテンプレートリテラルを「変数部分」で区切ったものになります。
従って${hoge}てすと${fuga}テスト${puni}というテンプレートリテラルの場合は第一引数と第二引数の値はそれぞれ以下のようになります。

index=0 index=1 index=2 index=3
第1引数 "" "てすと" "テスト" ""
第2引数 hoge fuga puni なし

タグ付きテンプレートリテラルを使って書き換える

先ほど例を書き換えていきます。
greetという関数を定義します。上記の解説にある通り引数は2つです。
第2引数のvalues内にnameの値が格納されているので、これが空なら空文字をreturnします。
そうでない場合は通常通りテンプレートリテラルの出力結果を返します。
第1引数も第2引数も 「テンプレートリテラルの中身を分割した配列」 なので、indexが若い順に結合していきます。

const greet = (hashes: TemplateStringsArray, ...values: string[]): string => {
    // values[0]が空なら空文字を返す
    if(!values?.[0]) return '';
    // 正常系は従来通りのテンプレートリテラルの結果を返せば良い
    // hashes[0] -> values[0] -> hashes[1]のように結合していく
    return values.map((value, index) => hashes[index] + value)
            .concat(hashes.slice(values.length))
            .join("")
}

nameの値がある時と無い時で比較してみましょう。
無い場合はコンソールになにも表示されなくなりました。

const name1: string = 'Zenn太郎';
const text1: string = greet`おはようございます。 ${name1} さん`;
console.log(text1); // --> "おはようございます。 Zenn太郎 さん"

const name2: string = '';
const text2: string = greet`おはようございます。 ${name2} さん`;
console.log(text2); // --> ""

まとめ

今回はTypescriptの実装例を見ながら 「タグ付きテンプレートリテラル」 について解説しました。

通常のテンプレートリテラルは知っているけど、こちらは馴染みが無い方も多いのではないでしょうか。
その理由のひとつに、使用されるケースが限定的であるということがあると思います。
実際、目にするのはstyled-componentsのようにライブラリの仕様として使われているケースがほとんどなので、自作するケースは稀だと思います。

いざ使うようになった時にこの記事が参考になれば幸いです。