Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

tagged template literals should be allowed (for completeness) #54

Closed
ScottRudiger opened this issue Mar 12, 2018 · 51 comments
Closed

tagged template literals should be allowed (for completeness) #54

ScottRudiger opened this issue Mar 12, 2018 · 51 comments

Comments

@ScottRudiger
Copy link
Contributor

From the current README:

The following are not supported due to a lack of real-world use cases:

optional construction: new a?.()
optional template literal: a?.`{b}`
constructor or template literals in/after an Optional Chain: new a?.b(), a?.b`{c}`

For this issue I'd like to understand:

  1. What are the reasons to NOT support tagged template literals?
  2. If there's no great technical issues with allowing them, WHY NOT support tagged template literals?

For discussion purposes, I'll provide a somewhat silly & simple toy problem-esque example:

/*
Prompt - In as few characters as you can, write a function 'splitOrJoin' that satisfies
two requirements:

1) If a string is passed in, split it and return an array of individual words, for example:
splitOrJoin('split this into an array'); // ['split', 'this', 'into', 'an', 'array']

2) If an array is passed in, join each element with spaces and return the resulting string:
splitOrJoin(['join', 'this', 'into', 'a', 'string']); // 'join this into a string'
*/

// Now your first instinct might be to write something with a ternary
const splitOrJoin = input => input.split ? input.split` ` : input.join` `; // this works

// Or, you could write it slightly differently
const splitOrJoin = input => input[input.split ? 'split' : 'join']` `; // also works

// But then you realize they're actually the same length when removing unnecessary characters
splitOrJoin=i=>i.split?i.split` `:i.join` ` // 43 chars
splitOrJoin=i=>i[i.split?'split':'join']` ` // 43 chars

// Hmm, how else could we shorten it? Oh yeah! There's this new thing called optional chaining
const splitOrJoin = input => input.split?.` ` ?? input.join` `; // also would work

// or the shortened version
splitOrJoin=i=>i.split?.` `??i.join` ` // 38 chars

// There! That's about as short as I can think to make it right now.

Now, you might think that code-golf is stupid (and I'd of course avoid using this pattern in production code--or anywhere you'd like your code to be readable and easily understood for that matter); but the fact is: tagged template literals are valid syntax in JavaScript.

Therefore, if you can write i.split` ` I'd also expect you to be able to write i.split?.` ` for the sake of completeness. Hopefully you can see past the ridiculousness of the example to see the reasoning for this.

So I'd like hear some logical argument around the two questions asked in the beginning. What are the reasons to arbitrarily (seemingly) disallow use of tagged template literals in conjunction with optional chaining? If there are, technically, no sound reasons for not supporting tagged template literals, why not allow them just like they're already allowed elsewhere?

@ScottRudiger
Copy link
Contributor Author

I'll start it off with a reason TO support tagged template literals.

I'll be honest; I was struggling, at first, to think of a real-world use case (other than code-golf, which is actually a thing that happens in the real world). However, I just thought of one possibility.

Suppose in the future (you want this feature to be as future-proof as possible, right?) there is far less need to transpile ES6 syntax to ES5 (i.e., Babel) as a greater and greater % of browsers in use provide native support. (Aside: Out of curiosity, here is the current state of support for template literals.)

In order to provide minifiers (such as UglifyJS) with all tools at their disposal to save bytes over the wire in your .min.js files, it might actually become important to support tagged template literals wherever possible, including after an optional chain--much like in the silly code-golf example.

@claudepache
Copy link
Collaborator

For this issue I'd like to understand:

  1. What are the reasons to NOT support tagged template literals?
  2. If there's no great technical issues with allowing them, WHY NOT support tagged template literals?

Adding optional tagged template literal support is more spec text, although it is easy to do. Therefore, I’ve chosen to drop them by default, i.e. unless/until there is consensus for adding them.

In other words, in absence of a good reason for one way or the other, I’ve picked the laziest option for the spec writer and (I expect) the implementors.

@ScottRudiger
Copy link
Contributor Author

ScottRudiger commented Mar 12, 2018

@claudepache

Adding optional tagged template literal support is more spec text, although it is easy to do. Therefore, I’ve chosen to drop them by default, i.e. unless/until there is consensus for adding them.

I see; I guess I'm not too familiar with the process yet. Would it be a reasonable request to suggest that any explainers/spec text clarify that template literal support is not required but is recommended for completeness's sake?

In other words, in absence of a good reason for one way or the other, I’ve picked the laziest option for the spec writer and (I expect) the implementors.

I'd hope the example I gave, while perhaps not the most compelling of reasons, at least nudges implementations to include support for tagged template literals.

@ScottRudiger
Copy link
Contributor Author

ScottRudiger commented Mar 12, 2018

I do fear, however, that if template literal support is not required in the spec that browser/Node.js, etc. implementations would become fragmented, only complicating things; for example, it would lead caniuse.com to cover three different features:

  1. template literal support
  2. optional chaining support
  3. optional chaining (followed by template literals) support

This would inevitably delay or prevent minifiers' ability to make use of their combining with optional chaining as in comment.

I'd probably be willing to accept that risk if it's at least strongly suggested to include template literal support, however.

Edit:
In any case, I had to get my thoughts on the matter out and I hope this issue will serve as a historical reference to any implementors deciding whether or not to add support for optional tagged template literals.

If anyone else can think of other use cases, feel free to jump in.

@alangpierce
Copy link

alangpierce commented Mar 12, 2018

@ScottRudiger I might be wrong, but to my knowledge, none of the JS spec is considered "recommended"; a feature is either in the language (meaning all JS engines will implement it) or not. For the reason you described, I don't think that type of thing would make sense in JS.

It's not just tagged template literals, there are lots of features that might be included here for the sake of completeness, like optional new, optional delete, and optional property assignment. Here's a previous discussion I started on the popularity of these features: #17 (The current state of the proposal is that optional delete is actually supported, I think.)

The way I think about this is that it's best to work incrementally, just like with code. A less ambitious proposal is inherently less controversial, so it'll make it through committee faster. Optional tagged template literals can always be added later. That's also why nullish coalescing is it's own proposal instead of being included here, even though the two are related. The case to be careful about, though, is when a proposal limits what changes can be done in the future. That's why so much thought has been put into avoiding the syntax clash between optional chaining and nullish coalescing, even though the two proposals are separate.

Optional chaining has been in stage 1 for over a year, despite significant work and despite it being very popular. For me, it's a bit frustrating watching it take so long, although it's certainly understandable that people want to get it right. There's already enough detail and controversy in the core feature that IMO it's best to keep it as simple as possible and leave any nice-to-have extensions for future proposals.

@ScottRudiger
Copy link
Contributor Author

@alangpierce

I might be wrong, but to my knowledge, none of the JS spec is considered "recommended"; a feature is either in the language (meaning all JS engines will implement it) or not.

I was imagining that to be the case. That's why I was thinking a benefit of requiring support would be to prevent different implementations supporting/not supporting the feature--a problem that seems to have contributed much complexity to JS in the past (for library writers/web devs/JS users in general).

It's not just tagged template literals, there are lots of features that might be included here for the sake of completeness, like optional new, optional delete, and optional property assignment.

I can definitely see your point regarding optional property assignment, as that is a separate feature that could be, and likely will be, discussed and separately introduced in another proposal down the line. However, I don't see tagged template literal support as necessarily a full-fledged feature on its own. It's more on the same level as optional function/method invocation, e.g., obj.meth?.() == obj.meth?.`` .

The way I think about this is that it's best to work incrementally, just like with code. A less ambitious proposal is inherently less controversial, so it'll make it through committee faster.

Thanks for the insight around the process for getting proposals through committee. I was surprised to see that @claudepache originally wrote this proposal over two years ago. As an aside, I'd like to commend the TC39 contributor/proposal writers as well as the TC39 members for their patience and persistence in trying to improve and extend JavaScript, with substantial time and effort on their parts.

The case to be careful about, though, is when a proposal limits what changes can be done in the future. That's why so much thought has been put into avoiding the syntax clash between optional chaining and nullish coalescing [...].

I totally agree; that's why I've been kind of obsessed with following and commenting in #51, #34, and #5, as I think it's very important to settle on a reasonable syntax for many reasons, including future considerations. (I'd encourage anyone reading to see those issues for discussion on syntax. Leave this thread to the issue at hand and I'll promise not to reiterate my preferred option here. 😉)

To address your point though, I don't think that requiring support for tagged template literals now does anything to limit future proposals, other than negating any need to introduce a proposal just to specifically add support in the future.

Conversely, I'd think my previous comment shows that requiring support now may actually save all involved from potential complexity and heartache if/when implementations diverge. 💔

I'm open to hearing what foreseeable changes, if any, could be limited by template literal support now, however. Let me know.

Thanks for your well-thought out reply.

@littledan
Copy link
Member

I like @claudepache 's minimalism approach here: It seems less likely to lead to confusion. I don't think it's possible for us to create a fully "orthogonal" optional chaining feature here--there are lots of situations where you might want short-circuiting or optional chaining that would be pretty infeasible or excessively complicated. Ultimately, programmers will have to learn what's allowed.

@claudepache
Copy link
Collaborator

claudepache commented Mar 14, 2018

At first sight, optional tagged template literal is tempting ”for completeness”. But on reflection, I think that:

a ?. `${x}` // undefined if a is nullish, a `${x}` otherwise

is about as reasonable or useful as:

a ?.* b // undefined if a is nullish, a * b otherwise

Also, one may also make sense of template literal optionally tagged, i.e. (random strawman syntax):

a???`${x}`// `${x}` if a is nullish, a `${x}` otherwise

@claudepache
Copy link
Collaborator

To be precise, ”Optional tagged template literal support” may be subdivided in two cases (although I don’t think that it is a good idea to support one and not the other):

  1. a?.b`c` (template literal appears inside an optional chain);
  2. d?.`c` (template literal appears at the beginning of an optional chain).

@ljharb
Copy link
Member

ljharb commented Mar 14, 2018

I think the former must be supported; the latter i think is a totally separate feature.

In other words, when deciding what to support, i think there shouldn’t be anything we don’t support inside an optional chain - only things at the beginning of an optional chain (ie, the things that are optional)

@ScottRudiger
Copy link
Contributor Author

i think there shouldn’t be anything we don’t support inside an optional chain - only things at the beginning of an optional chain (ie, the things that are optional)

I totally concur with this line of thinking. To me it seems like if the former (a?.b`c` ) is disallowed that optional chaining is kind of "stepping on" what one would normally expect to be allowed in JS; i.e., a.b`c` is already valid syntax so why is optional chaining preventing it?

I think the former must be supported; the latter i think is a totally separate feature.

However, I disagree--to a degree--with this. I think I already mentioned this (not quite awake yet), but a?.('c') can be considered equivalent to a?.`c` . Therefore, it's a separate feature in the sense that a?.c is a different feature from a?.['c'] is a different feature from a?.('c'). However, imo it's more like: because a?.('c') is a feature, I'd also expect a?.`c` to be a feature. They're literally the ~same thing.

@claudepache
Copy link
Collaborator

claudepache commented Mar 14, 2018

i think there shouldn’t be anything we don’t support inside an optional chain

It depends whether a template literal found at the end of an optional chain is considered to be inside it or after it. Surely, for a spec guru it is inside, but for a lambda programmer?

This is not really a issue if the application of the incorrect thinking would necessarily lead to a TypeError (and this is why I consider that short-circuiting up to the end of the chain is safe even for those that do not understand it). But regarding template literals, someone could think of:

a?.b`c`

as an optionally tagged template literal:

(a == null ? `c` : a.b`c`)

(Personally, I don’t think it is a real problem, but it is worth raising the question.)

I think the former must be supported; the latter i think is a totally separate feature.

I disagree. For someone not versed in the spec details, it would seem like an arbitrary restriction or an inconsistency if some construct is allowed in some location of an optional chain, but not in another. (Yes, I’ve changed my mind since I’ve written #4 (comment).)

@ScottRudiger
Copy link
Contributor Author

ScottRudiger commented Mar 14, 2018

I think the former must be supported; the latter i think is a totally separate feature.

I disagree. For someone not versed in the spec details, it would seem like an arbitrary restriction or an inconsistency if some construct is allowed in some location of an optional chain, but not in another. (Yes, I’ve changed my mind since I’ve written #4 (comment).)

👍 Totally agreed; this is what I've been trying to say, but in better, more concise words than I can ever dream to think of 🤣. I'd also add: it seems like an arbitrary restriction or inconsistency if some construct is allowed in a <regular> chain, but not in an <optional> chain.

This is not really a issue if the application of the incorrect thinking would necessarily lead to a TypeError (and this is why I consider that short-circuiting up to the end of the chain is safe even for those that do not understand it). But regarding template literals, someone could think of:

a?.b`c`

as an optionally tagged template literal:

(a == null ? `c` : a.b`c`)

But I honestly don't see how they could think of that meaning if they have an understanding of either optional chaining OR tagged template literals (read the docs). If they wanted to achieve a == null ? `c` : a.b`c` they could write something like:

a?.b.`c` ?? `c`

edit: or even without knowing about the null coalescing operator's existence:

a?.b.`c` || `c`

@ljharb
Copy link
Member

ljharb commented Mar 14, 2018

I think debating whether "invocation" and "tagging a template literal" are the same is separate; I'm still grouping things into "optional X" and "X behind an optional chain" - I think that everything should work in the "X behind an optional chain" bucket identically to how it would work with "X behind a normal chain" (when X is not nullish, obviously), and I think that we can and should have distinct discussions about supporting "optional X" for any given X's.

@claudepache
Copy link
Collaborator

@ljharb I'm still grouping things into "optional X" and "X behind an optional chain"

It’s not the first operation of the chain that’s optional, it’s the chain as a whole. (This is how short-circuiting works without resorting to artefact like Nil reference or abrupt completion.)

@ljharb
Copy link
Member

ljharb commented Mar 14, 2018

@claudepache yes, but the pivot point of the optionality is where i'm focusing on as the dividing line.

@claudepache
Copy link
Collaborator

@ljharb

yes, but the pivot point of the optionality is where i'm focusing on as the dividing line.

Yes, but I think I place that pivot point more precisely than you, i.e. more precisely than the formal grammar. In an expression like:

a??.b.c

I conceptually put the dividing line between the ?? (”optional”) and the first . (”normal property access”), although technically they are part of the same operator. From that point of view the first property access (.b) is not substantially different from the second one (.c).

@ljharb
Copy link
Member

ljharb commented Mar 14, 2018

Ah - I'm putting it between a??.b and c, conceptually - but I don't think that impacts my intuition much. In either case of:

a??.b`foo`

the act of tagging the template literal, even though it happens conditionally, isn't optional - it's required (if the chain continues).

@claudepache
Copy link
Collaborator

@ljharb

the act of tagging the template literal, even though it happens conditionally, isn't optional - it's required (if the chain continues).

In a??.b`foo` , the template literal isn’t more or less optional or required than the preceding property access: either both are triggered, or both are skipped. Again, it’s the whole chain that’s optional.

@rattrayalex
Copy link

FWIW, I would intuit

a??.b`foo`

to mean

a == null ? a : a.b`foo`

and would be very surprised if that were not to work.

@claudepache
Copy link
Collaborator

The example proposed in Comment #0 is not a valid use case, in the sense that neither String.prototype.split nor Array.prototype.join are suitable for tag expressions, and it is just a chance (or a hack) that they do the job. (In case you have not understood, just read the friendly manual, and realise that, in the present case, we are fortunate enough that ["bar"].toString() === "bar".)

I haven’t enough imagination in order to find even a semi-valid use case for optional tagged literal, and I expect that it would be a tough exercise.


Anyway, I stand on my position, that we should either reject both ”tagged literal in optional chain” (a?.b`c` ) and ”optional tagged literal (a?.`b`), or accept both. For the purpose of the discussion, let’s assume that some coder finds a use for the following setting: ”If obj has an "optionalTag" property, use it as a tag function; otherwise yield undefined”, and they write at first:

obj?.optionalTag `c`

It seems to work at first sight..., but the programmer realises soon their mistake, that they really mean:

obj.optionalTag?.`c`

It would appear more inconsistent that the first expression works and the second expression doesn’t, than the two expressions are equally rejected (or equally accepted).

Or, from another perspective, in the remote case we find some good reason for writing a?.b`c` , we will most probably find some good reason for writing d?.`e` .

@claudepache
Copy link
Collaborator

Here is a reason why I think we should reject tagged template in optional chain: A tag resembles more to a unary operator (as typeof) or a decorator than a regular function or method, and the fact that tagged templates are part of LeftHandSideExpression rather than UnaryExpression is a detail that most people can happily ignore. Reading a tag as a decorator/unary operator, the expression:

a?.b 
`c`

appears as if it should desugar to:

(a == null ? undefined : a.b)
`c`

Granted, this is completely useless, because

undefined
`c`

is always a TypeError. But maybe in some future we could find reasonable to treat undefined `c` as a synonym of `c`, in which case the above expression could make sense... TBH, the usefulness of undefined `c` seems highly improbable to me, but still less improbable than the usefulness of tagged template in an optional chain.

@ScottRudiger
Copy link
Contributor Author

Maybe I'm not following along, but I'd think that if template literals are allowed in/after an optional chain, ASI wouldn't apply à la #56, and

a?.b
`c`

would be interpreted as

a == null ? undefined : a.b`c`

Therefore, as written, there would be no chance for the result to be

undefined
`c`

because the whole optional chain would either short-circuit to undefined or a.b`c` .


I agree both

obj?.optionalTag `c`

and

obj.optionalTag?.`c`

should be allowed for consistency, rather than one or the other.

@claudepache
Copy link
Collaborator

@ScottRudiger

Maybe I'm not following along, but I'd think that if template literals are allowed in/after an optional chain, ASI wouldn't apply à la #56,

The ASI issue is independent from this discussion. Whatever the fate of template literals, we will prevent ASI from producing astonishing and/or forward-incompatible outcome.

and

a?.b
`c`

would be interpreted as

a == null ? undefined : a.b`c`

The answer is ”yes/no”, as the question contains ”in/after”... More precisely, given a?.b `c` :

  • if we decide that the template literal is in the optional chain, we’ll have
    a == nulll ? undefined : a.b `c` (for which we have zero use case as of today);
  • if we decide that the template literal is after the optional chain, we’ll have
    (a == null ? undefined : a?.b) `c` (which is objectively useless as of today);
  • if we choose neither, it will be a syntax error, but we’ll tweak the spec so that ASI won’t produce astonishing and forward-incompatible behaviour.

@getify
Copy link

getify commented Jun 6, 2019

I'd like to suggest that I have a use-case for including support of both:

a?.`b`

// and

a?.b`c`

I have been working on a type linter/checker for JS, as an alternative to TS/Flow, which uses template tags as annotations for value/expression types. It looks (in part) like this:

var x = string`${ someval }`;

This code is (optionally) checked at compile time, and if it can be verified statically, it's simplified to ship this for runtime code:

var x = someval;

But if it cannot be verified at compile time, then the tag funcion is left in, and serves as a runtime assertion. There's a runtime library that provides a global identifier string that is a tag function, which is invoked at runtime and checks that someval variable to ensure that it's actually a string.

If for some reason, you fail to include the runtime library, then string won't be a defined tag function, and you'll get a type-error.

It may be preferable for some users of my type linter that it convert the code to this before shipping to the runtime (this would be a configuration option for the tool):

var x = string?.`${ someval }`;

That way, if the runtime library is available, you get the assertion checking. But if the runtime library is missing, the tag call doesn't happen and throw the type-error.

Another case where they may have tag function calls in their runtime, but not have the tag functions defined, is in the case of user-defined types:

var x = mycustomtype?.`${ someval }`;

As to what semantics that would imply if the tag function is nullish, there are two options for what that statement would effectively produce in equivalent runtime behavior:

// option 1:
var x = `${ someval }`;    // no tag

// option 2:
var x = undefined;     // no template expression at all

If option 1 prevails, that's fine because you basically just get a x = someval pass-through, which is probably what my tool's users might expect. I personally think option 2 is more consistent mental-model wise, given the notion of short-circuiting in the rest of the ?. feature. Of course, that option does in fact create some potential footguns -- in which case that user shouldn't be relying on that config option!

Either way that semantic plays out, I think the desire to avoid a type-error on the missing tag function call could very well be the desired behavior for some of my tool's users.

It's also entirely plausible that some users of my tool may prefer to have these type-annotation tags be namespaced, like:

var x = types.string`${ someval }`;

So for the same reasoning as above, these users may prefer to configure the compiler to ship:

var x = types?.string`${ someval }`;

Or even:

var x = types?.string?.`${ someval }`;

Of course, I won't offer this config option in my tool if TC39 decides to not support these syntaxes. But I think these syntaxes should be supported, and I think this a use-case for why their support could be useful to some users.

@ljharb

This comment has been minimized.

@getify
Copy link

getify commented Jun 6, 2019

@ljharb I was believing/assuming that ?.` was the template-tag equivalent of the optional-call syntax ?.(. No?

@claudepache
Copy link
Collaborator

claudepache commented Jun 6, 2019

So, if I understand your use case, you want

null ?. `foo`

to be equivalent to:

`foo`

However, this is not what optional chaining does (that is to say: would do if template literals were allowed in that position). Indeed, given null ?. `foo` , as the LHS of ?. is nullish, it will short-circuit and evaluate to undefined rather than `foo`.


I see two ways to address your use case, none of them is related to optional chaining:

  1. Allowing a nullish value to be used as tag for tagged template. A template tagged by null is equivalent to a non-tagged template; so that you can just write:
var x = myPossiblyNullCustomType `${ someval }`
  1. Defining a predefined tag, say String.cooked, which does what the language does for untagged templates. So that you can write either (where ?? is the upcoming nullish-coalescing operator):
var x = (myPossiblyNullCustomType ?? String.cooked) `${ someval }`

or:

if (myCustomType == null)
    myCustomtype = String.cooked
var x = myCustomtype  `${ someval }`

Such a tag has already been proposed on esdiscuss.

@getify
Copy link

getify commented Jun 6, 2019

So, if I understand your use case, you want...

Nope. I called out in the middle of my message that the semantics could debatably be either of these:

`foo`

// or

undefined

But that personally I'd prefer the undefined semantics because of short-circuiting.

However, whichever way that semantic played out, I could see users of my tool wanting that optionality (aka, graceful non-error-throwing handling) instead of just a type error.

@getify
Copy link

getify commented Jun 6, 2019

However, this is not what optional chaining does. Indeed, given null ?. `foo` , as the LHS of ?. is nullish, it will short-circuit and evaluate to undefined rather than `foo`.

Unless I've missed something, I don't think optional chaining does that at all, yet. Wasn't that the point of this thread, to debate if tag?.`string` should be a supported syntax?

It's currently listed as unsupported, and my message was to argue a use-case for why it should be supported.

@claudepache
Copy link
Collaborator

Unless I've missed something, I don't think optional chaining does that at all, yet.

For sure, I meant: it is what optional chaining would do if template literals were allowed in that position. I’ll edit and add the conditional in my post.

The semantics of optional chaining is: Evaluate the LHS; if the value of the LHS is nullish, stop evaluating the current expression and yield undefined; otherwise continue evaluating the current expression normally. — All that is orthogonal to the “support” of template literals.

@getify
Copy link

getify commented Jun 6, 2019

I'm fine with that semantic, and I'm fine that template literals, if supported, would not have any special case handling in that respect.

I just want to re-emphasize that in my tool's use case, I can see why some would prefer the optional-tag-call resulting in silent/graceful undefined as opposed to having a type-error thrown.

It would be helpful to have both a?.`b` and a.?b`c` syntaxes supported for that purpose.

@claudepache
Copy link
Collaborator

I just want to re-emphasize that in my tool's use case, I can see why some would prefer the optional-tag-call resulting in silent/graceful undefined as opposed to having a type-error thrown.

I’ve re-read the use case you posted above. Unless I’ve missed something, given:

var x = types.string`${ someval }`; // decorated with some “optional” mark(s)

and supposing that types is missing, your user would want `${ someval }` rather than undefined, wouldn’t they? Yielding undefined would just silently propagate the error further, and some amount of code is still needed to be written in order to recover gracefully, e.g.,

var x = types?.string`${ someval }` ?? someval

which goes against DRY. A better alternative (in my opinion) is:

var x = (types?.string ??  String.cooked)`${ someval }`

which does not use template literal in the optional chain.

Or do you have another concrete example, that shows that the “produce undefined if tag is nullish” behaviour is indeed useful?

@getify
Copy link

getify commented Jun 6, 2019

@claudepache

your user would want `${ someval }` rather than undefined, wouldn’t they?

I am not sure that's the case. I think some might, but others might not. Some might want it, but be surprised by what they get and only realize later that this wasn't what they really wanted after all.

But regardless of whether they "want" that or not, that's not the semantic that would be documented as being provided. So it's moot with respect to our discussion here.

Yielding undefined would just silently propagate the error further, and some amount of code is still needed to be written in order to recover gracefully

Yes, this is absolutely true, but that's actually a great argument why it should be undefined rather just than the template literal expression, because in the case of silently not verifying the value (ie, not running the tag function), you're thinking it was verified when it wasn't; ugh, sneaky false positive.

By instead getting an undefined value, you're much more likely (and able!) to detect this problem and either register it as an error, or gracefully recover from it, as compared to just receiving the string from `${somevalue}` (worse: that's always a string, no matter what type somevalue was).

In other words, my claim is that at least some of my tool's users will prefer to detect/handle this problem scenario more silently/gracefully via the undefined value, than having to catch a thrown type-error as a result of the tag function missing.

If TC39 were to still decide not to support the ?.` syntax, then I think my tool should instead produce and ship something like this code:

// var x = typeof string == "function" ? string`${ somevalue }` : undefined;
// edit:
var x = string != null ? string`${ somevalue }` : undefined;

...which is exactly the proposed semantic of this feature. So of course, in this case, I'd far rather have it produce and ship:

var x = string?.`${ somevalue }`;

That's the main crux of my use-case argument in favor of this feature.


Moreover, I would claim that the existing spec'd behavior -- a?.() resulting in undefined if the a() function is nullish and can't be called -- is exactly the same situation as with tag functions: to properly handle this problem, both forms have to add some additional graceful handling of the undefined.

IOW, if this is useful/OK to expect this of usages of ?.(, then it should be equally useful/OK to expect it for usages of ?.`.

I don't see any compelling reason to favor/distinguish that undefined handling with one call form over the other. So I believe that argues for consistency here.

@ljharb
Copy link
Member

ljharb commented Jun 6, 2019

Based on optional call's semantics, it would not, in fact, check typeof function - it would check something more like string == null ? string : string${someValue};, so that it would throw if the value was a non-nullish non-function (just like the currently proposed optional call)

@ScottRudiger
Copy link
Contributor Author

@ljharb, I think @getify meant:

var x = string != null ? string`${ somevalue }` : undefined;
// vs
var x = string?.`${ somevalue }`;

Either way, IMO the use case is valid.

@getify
Copy link

getify commented Jun 6, 2019

@ljharb you're right, the check is (and should be) a nullish check. I was trying to be more descriptive about my thinking, but that came at the cost of precision.

@claudepache
Copy link
Collaborator

Moreover, I would claim that the existing spec'd behavior -- a?.() resulting in undefined if the a() function is nullish and can't be called -- is exactly the same situation as with tag functions: to properly handle this problem, both forms have to add some additional graceful handling of the undefined.

In the README of this repo, I’ve given two concrete examples for optional method calls. In the first one, no code is needed to handle the “problem”, because the problem is completely solved by the optional call. In the second example, nothing additional is needed in order to have “graceful” degradation—although in other similar cases, a simple ?? fallback would be added.

IOW, if this is useful/OK to expect this of usages of ?.(, then it should be equally useful/OK to expect it for usages of ?.`.

I don't see any compelling reason to favor/distinguish that undefined handling with one call form over the other. So I believe that argues for consistency here.

Until now, there haven’t been convincing use cases for optional tagged templates. Your use case might be the first one, but it has still too many “might”s for my taste.

Also, what you call consistency here is what I call completeness. Probably the reason is that I consider that “tagging a template” is not the same thing as “invoking a method”, even if both end up executing some function. (Other example of “(not) the same thing”: “constructing a class instance” (with new)—that one is not supported either, see #22.)

@getify
Copy link

getify commented Jun 7, 2019

I’ve given two concrete examples for optional method calls...no code is needed

OK, fair enough, those two specific kinds of cases don't require the special handling.

But certainly ?.( will be used, or at least considered, for lots of other function calls where in most cases that handling will be necessary. So as a general feature for general usage -- which is what I would expect -- it does imply that extra handling.

Also, what you call consistency here is what I call completeness.

I don't object to that characterization.

with new—that one is not supported either

Though I personally don't care about (or often use) new, for completeness sake, it seems like it also ought to be supported, with the same undefined short-circuiting.

@hax
Copy link
Member

hax commented Jul 26, 2019

@getify Sorry I don't very understand the case you provide. If var x = string?.`${ somevalue }`; result in var x = undefined instead of var x = somevalue, it's likely your user's code will throw error in some other place. Doesn't it just introduce much pains compare to throwing TypeError in first place?

@getify
Copy link

getify commented Jul 26, 2019

@hax I do not think it will lead to errors, I think it will prevent them.

As I said earlier, the chances that a tagged template being undefined, and then needing basic checks to see it's not a string/etc as expected to handle the error, are absolutely identical to the checks and handling you would have to do for any function call (or property access, for that matter) which was short-circuited to undefined.

If we can imagine those checks and handling for functions and properties, then we should be able to see that those patterns apply equally to tagged templates.

@getify
Copy link

getify commented Jul 26, 2019

var obj = {};

var a = obj.?numberOne;
// safety guard in case of short-circuiting
if (typeof a == "number") {
  console.log(a + 41);
}

var b = obj?.fortyOne.?();
// safety guard in case of short-circuiting
if (typeof b == "number") {
  console.log(b + 1);
}

// if we do the other two, we should do this one too
var c = obj?.toNum?.`41`;
// safety guard in case of short-circuiting
if (typeof c =="number") {
  console.log(c + 1);
}

@claudepache
Copy link
Collaborator

If you want to convince us, you should find a concrete scenario (or, better, a real-world use case) where an optional tagged template literal is useful and better than other alternatives. I am convinced that maybe it could happen that it might be useful someday to someone. But I am not at all convinced that a random occurrence of foo?.`bar` will not be more often a bug (i.e. a logic error from the programmer) than a useful expression.

Mere analogy to optional call is not sufficient. As a precedent, per #17, optional call has significant practical uses but optional construction has zero practical use.

@getify
Copy link

getify commented Jul 26, 2019

@claudepache

Mere analogy to optional call is not sufficient.

I am not justifying the request based on that. I gave the use-case as justification for the request. (see below)

I am defending against the "won't this create worse errors" dismissal, which is misguided. As I've asserted and demonstrated, the "oops this is 'undefined' instead of the value I wanted" handling is identical with property accesses, function calls, and tagged template calls, which is to say you of course are going to in general have to do that for all 3 cases.

Just because you refer to a few narrow use-cases cited which wouldn't need that extra handling does not justify your broader position -- or what I'm inferring is your position -- that tagged templates are of a different category, and thus don't hold weight here.

This is a feature you're adding to JS, the most widely used language on the planet, and that means people are going to use it for all kinds of general coding, not just a couple of specific scenarios you cite. That means they're going to use them with functions which return values, and if the short-circuting to undefined happens, it means that code is going to need to gracefully handle that because undefined won't be what they expected, and an error could happen if it's not handled.

They'll use ?.( in thousands of different call scenarios, and they'll write the basic guarding logic to handle the short-circuit scenario. That's all perfectly reasonable. And the exact same thing could (and should) be said of doing so with template tags.

If you want to convince us, you should find a concrete scenario (or, better, a real-world use case)

I already gave you a concrete scenario. The type-annotation library (called TypL) that I am building, uses template tags for annotating the types of literals... these tags both perform type assertions as well as value-type coercions. These tags are read by a compiler (if you choose) to do compile-time type linting (like TS), but the tags can also be used in the runtime code for dynamic value-type assertions.

The use-case again is:

var x = number?.`42`;     // **** if the operator is extended for template tags
if (typeof x == "number") {
  // do something with the number
}
else {
  // handle the lack of a number gracefully
}

If number tag function is defined and in scope, the tag works fine. If it's not there (they failed to load the tag assertions lib, etc), the only way to "handle" this, if the ?.` operator usage is not extended as requested, is:

try {
   var x = number`42`;   // if the operator is **not** extended for template tags
   // do something with the number
}
catch (err) {
  // handle the lack of a number gracefully
}

The latter of these approaches, requiring try..catch, is much less attractive than the graceful fallback to an easily detectable undefined value from the short-circuiting. It's less performant, and it's also more brittle, because there are various reasons that the exception can be thrown, so I'm probably going to have to do conditional logic on the err variable to check if it's related to the number template tag missing or something else.

The point, again, is that I and other users of TypL would like to be able to take advantage of ?.` short-circuting on these tag annotations, because the if..else handling is more graceful and friendly than the try..catch handling flavor.

Of course, there's no real code to point to doing this yet, since (1) this feature is only stage3 (2) the requested form is not supported (3) TypL is still under development. But if the requested form is added, I will add this support to TypL and then there will be real usages of the pattern.

@ljharb
Copy link
Member

ljharb commented Jul 26, 2019

fwiw the alternative would probably be closer to number && number`42` , since optionality isn’t about catching exceptions, it’s about safely navigating past null and undefined (number == null ? number : number`42` if you want to be pedantic or if the difference matters to your use case)

@getify
Copy link

getify commented Jul 26, 2019

@ljharb

That's one way of doing it, but I would say it completely breaks the ergonomic essence of this tag acting like a value-site type annotation, so it isn't how I would want to do it, nor do I think anyone who I expect to use TypL.

But your point of optionality is valid with respect to a default using the new ?? instead of an if statement:

var x = number?.`${ someVal }` ?? 42;

In many cases, that sort of usage would probably be far preferable to either the try..catch, if..else, or just letting an error happen, approaches.

@getify
Copy link

getify commented Jul 26, 2019

Other examples besides TypL... both Styled-Components and Lit-HTML use template tags. And the same "what if the tag function isn't present" scenario, along with a backup default value in that case, applies to them. Could be that the whole library didn't load, or could be that a custom style type being referenced that for some reason didn't get defined.

For example:

const Button = styled.Button?.`
     color: red;
  ` ?? defaultButton;

Here, if styled.Button is not present, the short-circuiting to undefined allows the ?? defaultButton fallback handling.

With the html tag from Lit-HTML:

const Button = html?.`<button> ... </button>` ?? defaultButton;

@mAAdhaTTah
Copy link

"Forgot to load the library" doesn't feel like a strong argument because in those cases number wouldn't be undefined, it would be undeclared, so doing:

number?.`42`

would be a ReferenceError (in strict mode). In order for this to be useful, the developer would have to:

  1. Use ES6+ syntax (for template strings).
  2. Execute their code in sloppy mode.
  3. Expect that the library may not load in all cases and desire a fallback in that case.

This feels like a really narrow use case to me.

@jridgewell
Copy link
Member

This feels like a really narrow use case to me.

Agreed. I think we're going to need a really strong usecase for the committee to agree to this, but even if we find one this should be done as a follow on proposal (the core ?. is important enough to ship it now).

@getify
Copy link

getify commented Jul 27, 2019

(edit: removed comments)

I'm going to drop out of this thread because there doesn't appear to be anything further to discuss.

@claudepache
Copy link
Collaborator

Now that we are at Stage 3, we can consider that the set of included operators is fixed. Optional template literal is not included, but the spec is forward compatible with an eventual future inclusion in case the need would be manifest.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants