Repository navigation
Type safety with TemplateStringsArray and tag functions #33304
Description
Activity
- addedIn DiscussionNot yet reached consensusNot yet reached consensusSuggestionAn idea for TypeScriptAn idea for TypeScript
on Sep 12, 2019 RyanCavanaugh commented
on Sep 12, 2019 MemberMore actionsDaniel Rosenwasser (@DanielRosenwasser) any pre-discussion thoughts?
DanielRosenwasser commented
on Sep 12, 2019 MemberMore actionsSeems related to #16552 (maybe it's a duplicate).
DanielRosenwasser commented
on Sep 12, 2019 MemberMore actionsI think the big problem is that you sort of want to create something that combines
TemplateStringsArrays with tuples of literal types, but it's not clear to me how to represent that. MaybeReadonlyArray<"some" | "strings"> & { raw: ["some", strings"], 0: "some", 1: "strings" }but that looks awful.
Reacted by Kipras Melnikovas, Fabian Cook, ExE Boss, Axel Rauschmayer and Vitali HaradkouNow that Template String Types are coming out, it would be great if the array was typed as a tuple of string literal types.
Then we would have strict typing and be able to do cool things like you can imagine from the following:
(Image courtesy of Okku (@0kku))
Reacted by Tony, ExE Boss, Rich. Cao, jakzo, Harry Solovay, Brian Kim, Max Makarov, wmzy, Vitor L Cavalcanti, Cameron Hunter and 20 moreWhat would great to have here is for
TemplateStringsArrayto be parameterized by the tuple type of it's literals.So the type of strings in:
html`<div>${'a'}</div>`
would be:
TemplateStringsArray<['<div>', '</div>']>
Reacted by Okku, a11delavar, Gavin Cannizzaro, Tyler Murphy, Sʜɪᴍᴜʀᴀ Yū, Jeff Zou, Denis Karabaza, Joey Mezzacappa, Patryk Wałach, Max Ruman and 23 moreIf we were to write
foo`one\\n${1}two${2}three`
we'd want to be able to work with a tuple type something like
type TheStrings = readonly ['one\n', 'two', 'three'] & { raw: readonly ['one\\n', 'two', 'three'] }
But how can a generic function work with a particular specific type if the type signature is generic?
So if we write something like
function foo<T extends string[]>(strings: T) {}
then the code we write in
foocan't do anything more specific withT.How would it work?
type TemplateStrignsArrayOf<T extends ReadonlyArray<string>> = TemplateStringsArray & T;
I'd imagine something like this would be fine?
As Trusktr mentioned above, I've been working on writing a fully featured XML type parser in TS, but the only missing puzzle piece is this issue. I'd like this:
declare function xml< T extends TemplateStringsArray, K extends ReadonlyArray<unknown>, > ( strings: T, ...slots: K, ): baz; xml` <div class='${foo}'>${bar}</div> `;
to have it infer
TasTemplateStringsArrayOf<["<div class='", "'>", "</div>"]>andKas[typeof foo, typeof bar]. Seems like such an obviously useful thing that it frankly didn't even occur to me that this wouldn't be supported already when I started writing the XML type parser.Reacted by strblr, ExE Boss, Anton Pieper, Hans Brende, Chandler Prall, Paul Melero, Kim, Jang-hwan, Jay Harris, Brian Kim, Derek Ziemba and 4 moreBut the issue with that approach is (unless I missed something) that the code can not forecast what type
Twill be, in order to type check it in its implementation. So even ifTwill be passed in as a specific type, the impl doesn't know that specific type, and we'd need to use type narrowing (which I think would mean many many permutations of conditionals, right?).Here's one way I think it could work:
When
foo`one${something}two`is called, TypeScript could pass the type of the string parts (["one", "two"]) as a generic intofoo, andfoo's implementation should have a constraint that type checks it.Example:
type AttributeNames = 'foo' | 'bar' | 'baz' type Attribute<S extends string> = `${AttributeNames}="${S}"` type TagNames = 'div' | 'p' | 'span' type OpenTag<T extends TagNames, S extends string = ""> = S extends "" ? `<${T}>` : `<${T} ${Attribute<S>}>` type CloseTag<T extends TagNames> = `</${T}>` type FullTag<T extends TagNames, S extends string = ""> = `${OpenTag<T, S>}${CloseTag<T>}` declare function html<T extends FullTag<TagNames, 'asdf'>[]>( strings: T, ...values: any[] ): any
// When TypeScript sees html`asdf`, TypeScript will call our function like the following: html([`asdf`]) // ERROR (good) // When TypeScript sees html`<div foo="asdf"></div>`, TypeScript will call our function like the following: html([`<div foo="asdf"></div>`]) // NO ERROR (good)
where in each
htmlcall, the type ofTis checked against the constraint that we defined. If not by checking constraints, how else can we do the actual type checks?That example is also fairly simple. How can we allow any attribute value, instead of hard coding particular attribute values like
asdfin my example?I tried to make the template more generic with
T extends string[](for testing) then tried to see if I could check the type of T with a function that does type narrowing with theisoperator, but my attempt failed. Can that be done?Titian Cernicova-Dragomir (@dragomirtitian) added return types, so in
const div = html`<div>...</div>` const p = html`<p>...</p>`
the type of
divwould be inferred toHTMLDivElement, andpwill beHTMLParagraphElement.What I mean by "would be" is that currently, it works only if we call
html(['<div>...</div>'])but not when we call it ashtml`<div>...</div>`.Duplicate of #31422?
Daniel Rosenwasser (@DanielRosenwasser) Ryan Cavanaugh (@RyanCavanaugh) Would you mind looking at this again (with the new example from above) in context of TS 4.1 Template String types?
This currently works:
const div = html(['<div>...</div>']) // div has implicit type HTMLDivElement, yaaay! const p = html(['<p>...</p>']) // p has implicit type HTMLParagraphElement, yaaay!
but this doesn't:
const div = html`<div>...</div>` // type error, ... is not assignable to TemplateStringsArray const p = html`<p>...</p>` // type error, ... is not assignable to TemplateStringsArray
(see the previous example for the idea)
This issue can be re-treated as a feature request to allow for template tag functions to receive better types when called as a template tag.
As the example shows, the template tag functions should be called with a similar type as when they are called as a regular function.
Reacted by Tom Picton, a11delavar, Stanley Stuart, orouz, Jiří Brabec, David Duong, Oliver Bell, Ivan Starkov, dgrbrady, Eloy Durán and 16 moreNow that #41891 has given template literals a template literal type, can that be extended so that TemplateStringsArray is generic on that type?
What we would want from that is to be able to compute the variables type based on the strings type:
export const html = <T extends TemplateStringsArray<S>, S extends TemplateLiteral>( strings: T, ...values: HTMLTemplateTypes<S>) => { // ... }
I think we would need a type like
TemplateLiteralwith utility types to extract the strings and values types, and in this examplesHTMLTemplateTypeswould compute the value types based on the string types.Reacted by David Enke, Alex Wayne, eczn*, Josh Junon, strblr, sam and Marvin HeilemannIs there any news on this?
Reacted by Andrii Dieiev and Martin JohnsReacted by a11delavar, varHarrie, Richard Kemp, Brian Kim, Rickvian Aldi, Azarattum, strblr and andrea-cerliniFwiw Daniel Rosenwasser (@DanielRosenwasser) the "not a full GraphQL parser" :-) use case that I'd like to use a type-literal-aware
TemplateStringsArrayfor is to make a type-safe version of the ruby%w[foo bar zaz]syntax.Here's a working example with a non-
TemplateStringsArrayfunction:type ToWords<S extends string> = S extends `${infer H} ${infer T}` ? H | `${ToWords<T>}` : S; function w<S extends string>(strings: S): ToWords<S>[] { return strings.split(" ") as ToWords<S>[]; } const words = w(`foo bar zaz`);
This types
wordsas["foo" | "bar" | "zaz"][]. Neat!But imo it would be even more neat as a tagged template:
const words = w`foo bar zaz`;Thanks!
(...also, reading #45310 it seems like people are already "parsing full GraphQL queries" with template type literals, by just using the same
gql("...query...")approach as I've done withw("foo bar zaz"). So it seems like the GraphQL parsing cat is already out of the proverbial bag, and I'm not sure it's worth the language inconsistency (that these two features don't work well together) to prevent what users are already working around/doing anyway.)Reacted by ExE Boss, Tobias Diez, Alex Boisvert, Richard Kemp, Justin Fagnani, strblr, Enderchief, David Enke, Joe Pea, Jay Harris and 2 moreTagged template literals should be typechecked as regular function calls.
declare const tag: <T extends readonly any[], U extends any[]>(t: T, ...u: U) => [T, U]; // code tag`1${2}3\u${'4'}`; // typechecked as if declare const literals: readonly ['1', undefined] & {raw: readonly ['1', '3\\u']}; declare const args: [2, '4']; tag(literals, ...args);
Return type of this call is inferred as
[ readonly ["1", undefined] & {raw: readonly ['1', '3\\u']}, [2, '4'] ]
so it doesn't seem like there is any loss of information anymore.
Also I'd like to comment on Design Meeting Notes. It's mentioned that use cases are not obvious. The most obvious use case is to get typed and context-aware substitutions into SQL queries.
for (const {...} of query`SELECT * FROM widgets WHERE id = ${id}`) { ... }
This would allow to finally get typed SQL queries in Node. Currently the only way to do typed SQL is to use some kind of an ORM.
Parser for SQL strings doesn't have to be implemented by direly abusing type system. It's sufficient to provide a set of overloads for aforementioned
queryfunction. It can be done either by a language plugin (for IDE support), or code generator running once in a while.Of course, there are other strings that need context-based substitution checking, such as GraphQL queries, XML/HTML templates and grammars' parser actions that cannot be typed at the moment.
The implementation should be quite straightforward, plays nicely with recent additions to template string literal inference, and would fix a long-standing issue of
TemplateStringArraybeing incorrectly typed.Daniel Rosenwasser (@DanielRosenwasser) Could you please reconsider it on the next design meeting?
Reacted by Michele Piccirillo, Brian Kim, Sébastien Vanvelthem, Jan Nicklas, Yoz Grahame, Stephen Brian King, Dmitrii Pikulin, Thomas Stokes, Charles Pick, Rickvian Aldi and 16 moreSeems like this almost works but not quite? The below errors with
Argument of type 'TemplateStringsArray' is not assignable to parameter of type 'readonly ["\n query Foo {\n Tweets {\n id\n }\n }\n"] & { raw: readonly ["\n query Foo {\n Tweets {\n id\n }\n }\n"]; }'. Property '0' is missing in type 'TemplateStringsArray' but required in type 'readonly ["\n query Foo {\n Tweets {\n id\n }\n }\n"]'.ts(2769) ... The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.import * as types from './graphql.js' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' const documents = { '\n query Foo {\n Tweets {\n id\n }\n }\n': types.FooDocument, } export function gql(source: string): unknown export function gql( source: '\n query Foo {\n Tweets {\n id\n }\n }\n' ): typeof documents['\n query Foo {\n Tweets {\n id\n }\n }\n'] // Try to define a static TemplateStringsArray export function gql( tmpl: readonly ['\n query Foo {\n Tweets {\n id\n }\n }\n'] & {raw: readonly ['\n query Foo {\n Tweets {\n id\n }\n }\n']} ): typeof documents['\n fragment Lel on Tweet {\n id\n body\n }\n'] export function gql(source: string | TemplateStringsArray) { return (documents as any)[ typeof source ==='string' ? source : source[0] ] ?? {} } // works const t = gql('\n query Foo {\n Tweets {\n id\n }\n }\n') // fails const t2 = gql`\n query Foo {\n Tweets {\n id\n }\n }\n`
Reacted by Pavlos Vinieratos, Bae Junehyeon, David J. Hamilton and SancheZzIt looks like template string types are somehow "lossy"?
Constant strings and numbers are widened to just strings and numbers, and constant string fragments are flattened to
TemplateStringsArray- while the latter is clearly working as you intended it to, there must be some way we can open this up to more exciting use cases.My hope is we would be able to parse SQL template literals at compile-time, so that e.g.
sql`SELECT name, age FROM users`could be inferred asArray<{name: string, age: number}>based on a schema-type bound tosql.Someone attempted to start a project here, but it works with string literals only - but imagine the awesome simplicity and power of something like
sql-template-stringswith that level of type inference.(I'm aware of kysely, which is definitely impressive, but this approach is a lot more complex than adding compile-time type-checking over the ~50 lines of run-time code required for SQL template strings.)
Reacted by Karl Horky, Azarattum, Richard Kemp, Witold Sosnowski, Hans Brende, Paul Grau, Quinn Blenkinsop, Michael Nahkies, Robin, Jay Harris and 4 moreThis has bugged me for a while. I'm faced with two options:
- I don't use a tagged template & miss out on syntax highlighting. But the return type is correct.
- I use a tagged template & I get syntax highlighting, but no typing.
My work around is a hack where the first value passed into a tagged template is the expected return type. The only thing stopping things from working is
TemplateStringsArraywidening to string & losing the literal type. If it was just a readonly array or even just an array, it'd work fine as you can see from my example where I just call the template function with parenthesis & it can pick up on the type.I'm using JSDoc & plain JS as I find TS often just gets in the way.
/** * @template {string} T - The string to be trimmed. * @template {string} [C=' '|'\t'|'\n'] - The characters to be trimmed from the start of the string. Defaults to space, tab, or newline. * @typedef {T extends `${C}${`${C}${C}${C}`|`${C}`|''}${infer X}` ? TrimStart<X, C>: T} TrimStart */ /** * @template {string} T - The string to be trimmed. * @template {string} [C=' '|'\t'|'\n'] - The characters to be trimmed from the start of the string. Defaults to space, tab, or newline. * @typedef {T extends `${infer X}${`${C}${C}${C}`|`${C}`|''}${C}` ? TrimEnd<X, C>: T} TrimEnd */ /** * @template {string} T - The string to be trimmed. * @template {string} [C=' '|'\t'|'\n'] - The characters to be trimmed from the start of the string. Defaults to space, tab, or newline. * @typedef {TrimEnd<TrimStart<T, C>, C>} Trim */ /** * @template {string} HTML * @template {any} D - Default return value if fails to parse * @typedef {Trim<HTML> extends infer S extends string ? * S extends `<${infer K extends keyof HTMLElementTagNameMap} ${string}` ? HTMLElementTagNameMap[K] : * S extends `${string}</${infer K extends keyof HTMLElementTagNameMap}>` ? HTMLElementTagNameMap[K] : * S extends `<${infer K extends keyof HTMLElementTagNameMap}>` ? HTMLElementTagNameMap[K] * : D * : D} ParseHTMLElementType */ /** * @type {{ * <VALUES extends readonly [(abstract new (...args: any) => unknown) | typeof Array | typeof HTMLCollection | typeof String, ...any[]] * >(raw: TemplateStringsArray, ...values: VALUES): * VALUES extends [(abstract new (...args: any) => infer V), ...infer _] ? V * : VALUES extends [infer V extends Element | Element[] | HTMLCollectionOf<Element>, ...infer _] ? V * : DocumentFragment; * <TEMPLATE extends string | string[]>(raw: TEMPLATE): * ParseHTMLElementType< * Trim<TEMPLATE extends string * ? TEMPLATE * : TEMPLATE extends readonly string[] * ? import('../types/ts-toolbelt/sources/String/Join').Join<TEMPLATE> * : never * >, void> extends infer E extends HTMLElement ? E * : DocumentFragment; * <TEMPLATE extends string | string[], VALUES extends readonly string[]>(raw: TEMPLATE, ...values: VALUES): * ParseHTMLElementType< * Trim<TEMPLATE extends string * ? TEMPLATE * : TEMPLATE extends readonly string[] * ? import('../types/ts-toolbelt/sources/String/Join').Join<TEMPLATE> * : never * >, void> extends infer E extends HTMLElement ? E * : VALUES extends [(abstract new (...args: any) => infer V), ...infer _] ? V * : VALUES extends [infer V extends Element | Element[] | HTMLCollectionOf<Element>, ...infer _] ? V * : DocumentFragment; * }} */ // @ts-ignore var html = function html(raw, ...values) { let specificType; if (raw.length > 0 && raw[0] === '' && values[0] != null) { if (values[0].prototype instanceof Element || values[0] === Array || values[0] === HTMLCollection) { specificType = values[0]; raw = raw.slice(1); values.shift(); } else if (values[0] === String) { values.shift();// @ts-ignore return String.raw({ raw: raw.slice(1) }, ...values); } } const template = document.createElement('template'); template.innerHTML = String.raw({ raw }, ...values); const fragment = template.content; if (specificType === HTMLTemplateElement) { // @ts-ignore return template; } if (specificType === HTMLCollection) { // @ts-ignore return fragment.children; } if (specificType === Array) { // @ts-ignore return Array.from(fragment.children); } if (specificType === DocumentFragment) { // @ts-ignore return fragment; } if (fragment.childElementCount === 1) { // @ts-ignore return fragment.firstElementChild; }// @ts-ignore return fragment; };
Reacted by Vitali HaradkouReacted by andrew jarrettSo it's 2025,
typescript-gohas been announced and all sorts of well-meaning abuse of the type-system, such asarktype, has been normalised.The last discussion shelved the PR #45310 because it was uncertain whether the TS team wanted to support slow DSLs in the TS type-system, and while this concern is still true, I think the circumstances have changed since.
Could it be time to consider reconsidering the PR?
Reacted by Mayank, Azarattum, Marvin Heilemann, sosoba, Jay Harris, Djobbo-Victor, Justin Grant, Rasmus Schultz, Pavel Valadzko, Alistair Smith and 32 moreReacted by Marvin Heilemann, sosoba, tone, Djobbo-Victor, Justin Grant, David Enke, mathieulj, Alistair Smith, 9Morello, Dorbn4 Team and 14 moreAnother use case here that doesn't involve expensive parsing of the string literal spans is Zod 4's new template literal types: https://zod.dev/api?id=template-literals
In Zod you have to write your template literal types like:
const hello = z.templateLiteral(["hello, ", z.string()]); // `hello, ${string}`
In order to infer the correct type.
It would be a lot nicer to be able to write the Zod type as a template literal itself:
const hello = z.templateLiteral`hello, ${z.string()}`;
Reacted by Azarattum, Jan Potoms, Justin Grant, Marvin Heilemann, Neil, a11delavar, Kipras Melnikovas, Max Kamps, Richard Kemp, David Curwen and 13 more- added a commit that references this issue
on Jan 27, 2026 This issue will close once commit bfa230d is merged into the 'ms/const-template-literal' branch.bfa230d In Zod you have to write your template literal types like:
const hello = z.templateLiteral(["hello, ", z.string()]);
//hello, ${string}In order to infer the correct type.
i've written a little patch for the type checker and also wrote some Zod-like const template literal schema inference as a proof of concept:
would we be interested in this as a pull request to mainline TypeScript? turning
TemplateStringsArrayfrom an interface to a type alias is kinda gnarly.Reacted by Alexey Iskhakov, RanolP, Azarattum, Jake Lazaroff, unek, Eugene, Vitali Haradkou, AWCostabile and Jacob TorrångReacted by Eli, RanolP, Azarattum, Niki, Rasmus Schultz and AWCostabile



Search Terms
TemplateStringsArray,type,safety,generic,tag,functionSuggestion
Hello, I'd like to know how could I achieve type safety with
TemplateStringsArrayand tag functions or make such scenarios possible.Use Cases
I would use
TemplateStringsArraytogether with theCurrently, there's no way to do this, since
TemplateStringsArrayis not genericAND
gives the following error, completely preventing the type-safe usage:
Examples
The working case
I have some type-safe
i18ntranslations:I have a function to get the translations:
And I can safely use it with type-safe translations in the following fashion:
The problem
However, now I'd like to use the template string (template literal) syntax, paired with the tag function to achieve the same functionality, so I improve the
selectTranslationfunction:Possible solutions
I've tried 3 different solutions, neither of them giving me the results that I want:
a) Change the
TemplateStringsArrayto be a generic like soand used it like so
however, the same problems persisted - the casting from
keytorealKeyfailedAND the tagged function usage still failed
b) Instead of using
TemplateStringsArray, just useArray<K>the first problem of casting from
keytorealKeydisappeared,BUT the second one still remained
c) Just use
anywhich then allows me to use the tag function, BUT there's NO type-safety, autocompletions etc., making it practically useless.
TL;DR:
Neither of the solutions helped - I'm still unable to use the
selectTranslationfunction as a tag function with type safety.Is there any way to make it possible?
Reminder - we have 2 problems here:
Array<K>)TemplateStringsArraydoes not work like it should (or I'm using it wrong), even when used as a genericChecklist
My suggestion meets these guidelines:
I'm happy to help if you have any further questions.