Hacker News new | ask | show | jobs
by dmix 1203 days ago
Is it me or are Tagged templates a recipe for hard to read code?

Just the way it extracts the substrings into arguments for unpredictable strings. It doesn't translate to readable code.

I'd much rather use repetitive template strings. Unless I was doing some really fancy string manipulation. Or make my own functions with explicit arguments.

5 comments

They've allowed "styled-components" to work though, which is pretty nice. I always thought of them as providing a way to parse DSLs directly in your code. For example, a fictional way to generate some config could look like this (using tagged templates):

  const thingies = ["a", "b"];
  const config = yaml`
    - foo
    - bar
    - baz
    - thingies: ${thingies.map(thing => yaml`- thing: ${thing}`)}
  `;
  // config = ["foo", "bar", "baz", { "thingies": [{"thing": "a"}, {"thing": "b"}] }]
At a company I worked at people were generating YAML using Jinja templates, but tagged templates to me would be a better approach. Is it hard to read? It can be, but compared to the alternatives it's not too bad.

  const thingies = ["a", "b"];
  const thingToYaml = thing => 
    yaml`- thing: ${thing}`
  const thingiesYaml =
    thingies.map(thingToYaml)

  const config = yaml`
    - foo
    - bar
    - baz
    - thingies: ${thingiesYaml}
  `;
It doesn’t have to be hard to read
In this example, why use YAML at all?...

    const thingies = ["a", "b"];
    const config = [
      "foo",
      "bar",
      "baz",
      {
        thingies: thingies.map(thing => ({ thing })),
      },
    ]
    // config = ["foo", "bar", "baz", { "thingies": [{"thing": "a"}, {"thing": "b"}] }]
Right, it's not a great example since yaml is intentionally equivalent to JS objects.
Well, styled components also has a plain object syntax which I think a lot of people prefer me included.
To me, the single best example, is a function that turns a template into a parameterized query, and then makes an async database request...

    var result = await query`
      SELECT * FROM foo.bar where baz = ${baz}
    `;
There are libraries that allow for construction of queries as well using template strings. I don't know of too many instances beyond this where there's custom functions for tagged templates, vs just string building with parameters.
I agree this is pretty much the best example of this feature in action... which is why I don't think it should have been made a language feature.

It's nifty, but not nearly better enough to justify its existence, IMO. Here's the alternative:

    var result = await query(
        'SELECT * FROM foo.bar where bar = :baz',
        {baz}
    );
I get it... there's that extra level of indirection. But people are working hard, as we speak, to abuse the feature.
I'd rather have the language settle on one single templating syntax rather than every library and their son bake a half-assed one. "Oh, does query use the : syntax? The $ one? Does it take the template string first, or the arguments?" And with your example, `query` needs to figure out how to parse the string, extract the template slots, and pass the correct arguments into the correct slots. It's a recipe for disaster if every library needs to reimplement that.
Well, people could settle on a common templating syntax without making it a language feature. The fact that they haven't tells you it's not that important, relative to other concerns.

It's not like "figure out how to parse the string, extract the template slots, and pass the correct arguments into the correct slots" is rocket science.

And it's not like people are going to rewrite the numerous existing libraries for this kind of thing. The new tagged-template APIs are going transform their arguments and call the existing APIs.

I guess it's nice that new Javascript-specific templating languages can have common escaping syntax. It's just hard to get excited about the 15th standard.

I like tagged template strings but nowadays most libraries use TS (or at least JSDocs) and any serious IDE will be able to quickly answer your question of what type of parameters a function is expecting
In this case - the thing I personally value in the template version is I don't have to name the parameters and specify them in a separate place. It's especially useful in larger queries.

    var result = await query(
        'SELECT * FROM foo.bar where bar = :baz
          -- 100 more lines of where clauses, CTEs, etc.
        ',
        {baz} // where did I use this again?  I'd need to scroll up.
    );
versus

    var result = await query(
        sql'SELECT * FROM foo.bar where bar = ${baz}
          -- 100 more lines of where clauses, CTEs, etc.
        `);
Can confirm. This is the best use case I've come across. You can dynamically compose queries and not even think about argument positioning. No concern about injection. Better performance than named variables that are translated into positional.
I find them to be really useful for simple and targeted tasks. For example, simplur[1] for pluralizing strings and dedent[2] just for developer ergonomics.

[1] https://www.npmjs.com/package/simplur

[2] https://github.com/dmnd/dedent

Is there a difference between

   dedent`something ${code}`
and

   dedent(`something ${code}`)
? Not sure to understand the advantage of tagged strings here...
The difference is on the tag side. "Tag functions" receive the string segments separately from the interpolated values, so like:

    function exampleTag(stringSegment, ...values) {
      console.log(stringSegment);
      console.log(values);
    }

    const myNumber = 123;
    
    exampleTag`foo ${myNumber} bar ${myNumber}`;

    // Prints:
    // ['foo', 'bar', '']
    // [123, 123]
This gives the tag function a way to access the interpolated values themselves, before they get coerced to a String. You're correct, though, that it really doesn't make much of a difference for a dedent function.
It is actually important for a dedent function when the interpolands contain newlines. Consider this example:

    function fmt(q, a) {
      return dedent`\
        Question: ${q}
        Answer: ${a}
      `;
    }
Since `dedent` is basically syntactic sugar for developer convenience, we want this function to be equivalent to this:

    function fmtDesugared(q, a) {
      return `Question: ${q}\nAnswer: ${a}\n`;
    }
Now consider this input:

    console.log(fmt("a\nb", "c\nd"));
    // should print:
    Question: a
    b
    Answer: c
    d
But if `dedent` only got to see the string after interpolation—like this—

    function fmtWrong(q, a) {
      return dedent(`\
        Question: ${q}
        Answer: ${a}
      `);
    }
then the input to `dedent` would be

      Question: a
    b
      Answer: c
    d
and so `dedent` would not strip any indentation. That is, `dedent` is meant to identify the maximal common leading indentation in the template as written by the developer, which should not depend on the values of the interpolands.

A stale GitHub issue on the dmnd/dedent repo has a real-world example where this matters and led to a subtle bug: https://github.com/dmnd/dedent/issues/22

That is an absolutely fantastic point, and one I hadn’t considered. Thanks for the explanation, and also for teaching me the word “interpolands”. :)
Matching up the template strings with the values is a little wonky because there’s always one more string than value. But it’s pretty rare that you’d write lots of different tagging functions, usually you write a generic one to use with many different types of templates. Tucking away complexity into a function to make nice and easy-to-use templates is a reasonable trade off I think.
I love tagged templates and am so relieved to finally have something like Perl/Ruby/PHP's heredocs.

We should be able to write multi-line strings without having to break our linter's tab rules or write some silly post processing function!