GitHub - tc39/proposal-string-cooked: ECMAScript proposal for String.cooked built-in template tag (original) (raw)
String.cooked proposal
Champions:
- Jamie Kyle (Rome)
- Hemanth HM
Authors:
- Darien Maillet Valentine
Stage: 1 (Tracking Issue)
You can browse the ecmarkup outputor browse the source.
Overview
This proposes a new static String method like String.raw
but which concatenates the “cooked” (escaped string value) strings rather than the raw strings — the same behavior as that of an untagged template literal.
String.rawconsuming \u0072aw or undercooked strings may increase your risk of stringborne illness
;
// → "consuming \u0072aw or undercooked strings may increase your risk of stringborne illness"
String.cookedmmm ... \u0064elicious cooked string
;
// → "mmm ... delicious cooked string"
This can be used to simplify the creation of new template string tags:
function myTag(strings, ...values) { return String.cooked(strings, ...values.map(value => String(value).toUpperCase()) }
myTaghello ${'world'}
// "hello WORLD"
Motivation
Many template tags are interested in some kind of preprocessing of either the “cooked” string values, the substitution values, or both — but ultimately they may still want to perform the “default” concatenation behavior after this processing.
This can be achieved today in at least two ways:
- Implementing the “zip-like” concatenation behavior for the string values and substitution values “manually”.
- Delegating to
String.raw
.
The latter is very attractive; it’s the only exposed way to get something that_looks like_ the “default” behavior. That it’s actually different is not super obvious because for most input strings people are likely to test the output will be the same; the difference isn’t apparent unless you feed it literal segments which contain escape sequences (or “uncookable” escape-like sequences). The combination of raw
being present and no counterpart for the “cooked” behavior being present creates a sort of pit-of-failure.
Delegating to raw
is possible — but it requires passing the cooked strings as if they were raw strings, i.e. String.raw({ raw: strs }, ...subs)
instead ofString.raw(strs, ...subs)
. Though this works, the indirection is confusing; there aren’t any “real” raw string values in play here.
It may also be tempting for folks to use
String.raw
as if it were a true identity function for other reasons, as can be seen inthis Twitter post. Again, it seems pretty understandable why folks might see the one built-in tag and assume that it’s what they’re looking for, but hopefully withcooked
present as well, the distinction being made will become more apparent.
Use cases
The primary use case is to serve as a final step in custom template tags which perform some kind of mapping over input. For example, consider a tag which is meant to escape URL path segments in such a way that they round trip (i.e., any/
, ?
, and #
characters in the interpolated portions get percent-encoded):
function safePath(strings, ...subs) { return String.cooked(strings, ...subs.map(sub => encodeURIComponent(sub))); }
let id = 'spottie?';
safePath/cats/${ id }
;
// → "/cats/spottie%3F"
In other words, although it has the signature of a template tag function, it is mainly expected to facilitate building other template tags without needing to reimplement the usual string/substitution concatenation logic.
As a tag in its own right, it acts like the template tag equivalent of the identity function, which may also help with usage patterns like the example linked to earlier where the user wished to use template tags to provide a signal to their editor that the content should be interpreted as HTML. Some compilation or preprocessing tools may also benefit from that, e.g. Prettier singles out the tags with the binding “html” for different string transformation.
Q&A
Should the name be “cooked”?
Not sure! This is an initial proposal. Feedback about whether this name is intuitive and clear would be helpful. The term does have a history of usage in discussion contexts (ES Discuss, etc) as the counterpart for “raw,” but it has not appeared in any spec text or API surface to date as far as I know.
What is the behavior if undefined
is encountered when reading properties from the first argument object?
The tentatively proposed behavior is that if undefined
is returned when reading one of the index-keyed properties, a TypeError
is thrown. For any other value type, ordinary ToString
conversion is attempted.
This is because (assuming the first argument is a “real” template array object)undefined
appearing indicates that the raw segment source containedNotEscapeSequence, i.e. it is a template which has a raw value but does not have a cooked value. If such a template literal is untagged, a SyntaxError
would be thrown (though that would likely not be appropriate for an evaluation-time API, hence use ofTypeError
instead).
Arguably it could instead throw for any non-String value (unidiomatic) or it could not throw for undefined
. The rationale for the currently proposed behavior is that it aims to balance footgun prevention with other ergonomics concerns.