Typescript Tagged Template Strings

How to use template strings as functions

The basics: Template Strings

Before talking about tagged template strings, also called template literals, I just want to give a quick introduction to template strings in general. Feel free to skip to the next chapter if you already know what this is about.

Template strings are a special type of string symbols that can contain Javascript expressions as well span across multiple lines. They use the backtick-characters instead of double quotes, as it’s the case for common strings.

Regarding the spanning of characters across multiple lines, a common string has to contain a backslash in combination with the character “n” to create a new line. With template strings, you can simply create a new line “inline”, so to speak.

// A common string as reference.
const string = "I'm just a string";

// A template string. For Typescript,
// this value is a plain 'string'-type.
const templateString = `I'm just a string as well`;

The availability of expressions inside a string can also be quickly explained. Instead of just allowing string symbols, template strings accept arbitrary expressions. The following example shows what I mean by that.

function greet(name: string) {
  // Here you see an expression
  // *embedded* inside a string.
  return `Hello, ${name}!`;
}

// "Hello, Tom!"
const greeting = greet("Tom");

// One more example, using
// Typescript's 'rest'-operator
// which allowes any number of values
// and provides them as an array,
// in our case of type 'string'.
function greetAll(...names: string[]) {
  return `Hi everybody: ${names.join(", ")}!`;
}

// "Hi everybody: Tom, Luke!"
const greetingForAll = greetAll("Tom", "Luke");

Tagged template strings

This set of features alone would already be nice, but template strings can also be used as functions. You simply put a keyword in front of the template string to “tag” it, hence the name “tagged template strings”. Time for the next example.

// All previous examples used 'untagged'
// string literals, which means they're 
// practially just a string.
//
// But let's see how we can convert them
// to an acutal function:
function merge(template: TemplateStringsArray, ...params: string[]){
  
  // This needs some explanation:
  //
  // 'template', our first param, contains
  // all strings *inbetween the paramters*,
  // you'll see in a minute what I mean by that.
  //
  // 'params' then is an array of strings
  // that were provided as paramteres to the
  // template string.
  
  // Let's ignore the result for now.
  return "";
}

const what = "test";

// Here's the tagged template string in action.
// The tag is the function name, and like a 
// function the tagged template string can be called.
//
// Let's destruct how this will look like
// in the function defined above.
//
// 'template' = ["Just a", ""];
// 'params'   = ["test"]
const result = merge`Just a ${what}`;

// As you can see, the function splits the string
// into the string-only parts and the expressions.

As you can see, the syntax looks quite interesting and maybe even a little foreign when first working with it. There are probably not a whole lot of use cases that would require you to implement tagged template strings, but nonetheless we can play around with it. In the following and last example, I’ve put together some case studies. As you’ll see, tagged template strings can naturally be used with generics and open some interesting options for implementing certain requirements.

// 
// Generic
//
// Tagged template literals can be generic, too.
function generic<T>(template: TemplateStringsArray, ...params: T[]){
    return template.join(",") + params.join(",")
}

// "A value: ,test"
console.log(generic<string>`A value: ${"test"}`);

//
// Generic (with super powers)
//
// You can specify each type and 
// also limit the number of params!
function coalesce<A, B, C>(template: TemplateStringsArray, ...params: [A, B, C]){
    return template.join(",") + params.join(",")
}

// ", - , - ,value,0,true" 
const res = coalesce<string, number, boolean>`${"value"} - ${0} - ${true}`;

//
// Different return type
//
// Also, tagged literal types don't 
// only have to return strings.
const preview = (template: TemplateStringsArray, ...params: number[]) 
  => (template2: TemplateStringsArray, ...params2: string[])
  => {
    return "what?";
}

// Note the two pairs of backticks
// after each other, we're just calling
// the returned tagged template string
// form our first one!
console.log(preview`${0}``${"a"}`);

I hope you enjoyed this rather quick field trip into one of Javascript’s as well as Typescript’s feature. Template strings are most likely a common thing in your codebase, tagged template strings probably not, so it’s interesting to learn more about such niche features.

Suggestions

Related

Addendum

Languages