Computing tag functions for ES6 template literals

[2016-11-01] dev, javascript, esnext, template literals
(Ad, please don’t block)

This blog post describes what you can do with functions that return tag functions for ES6 template literals.

For an introduction to template literals, tagged template literals and tag functions, consult chapter “Template literals” in “Exploring ES6”.

Calling values via template literals  

The common way of calling a value in JavaScript is to append arguments in parentheses:

> const value = x => x;
> value(123)
123

In ES6, you can additionally call values via template literals:

> value`abc`
[ 'abc' ]

value is now a tag function whose first parameter is an Array with template strings and whose remaining parameters are the substitutions.

Functions that return tag functions  

If the value you call via a template literal is a function that returns a tag function then you can use arguments for the former function to parameterize the latter function.

For example: In the following interaction, repeat(x) returns a tag function that repeats its template literal x times:

> repeat(3)`abc`
'abcabcabc'
> repeat(3)`abc${3+1}`
'abc4abc4abc4'

This is an implementation of repeat():

/**
 * Tag function that returns a string that conforms
 * to the normal (“cooked”) interpretation of
 * template literals.
 * `String.raw` is similar, but follows the “raw”
 * interpretation.
 */
function cook(strs, ...substs) {
    return substs.reduce(
        (prev,cur,i) => prev+cur+strs[i+1],
        strs[0]
    );
}

function repeat(times) {
    return function (...args) {
        return cook(...args).repeat(times);
    };
}

Tag functions that return tag functions  

You can even create tag functions that return tag functions, enabling you to chain template literals. For example, this is a tag function three that lets you chain three template literals:

> three`hello``world``!`
'helloworld!'

This is how you’d implement three:

const three = (...args1) => (...args2) => (...args3) =>
    cook(args1) + cook(args2) + cook(args3);

The following tag function concat lets you create chains of arbitrary length, but you have to signal the end of the chain via an empty parameter list:

> concat`this``is``a``test``!`()
'thisisatest!'

This works, because template literals always provide at least one argument:

> ((...args) => args)``
[ [ '' ] ]
function concat(...args) {
    return concatRec('', ...args);
    function concatRec(accumulated, ...args) {
        if (args.length === 0) {
            return accumulated;
        } else {
            return concatRec.bind(undefined, accumulated+cook(...args));
        }        
    }
}

Real-world example: styled-components  

styled-components by Glen Maddern and Max Stoiber provides “visual primitives for the component age”. It lets you style React and React Native components via CSS in tagged template literals.

Example from the website:

import styled from 'styled-components';
import { Link } from 'react-router';

const StyledLink = styled(Link)`
  color: palevioletred;
  display: block;
  margin: 0.5em 0;
  font-family: Helvetica, Arial, sans-serif;

  &:hover {
    text-decoration: underline;
  }
`;

Further reading