Repository navigation
Type safety with TemplateStringsArray and tag functions #33304
Description
Search Terms
TemplateStringsArray, type, safety, generic, tag, function
Suggestion
Hello, I'd like to know how could I achieve type safety with TemplateStringsArray and tag functions or make such scenarios possible.
Use Cases
I would use TemplateStringsArray together with the
tagFunction`<type_safe_accessor>`Currently, there's no way to do this, since
- the definition of
TemplateStringsArrayis not generic
// source code:
// TypeScript/src/lib/es5.d.ts
// TypeScript/lib/lib.es5.d.ts
// local install:
// /usr/lib/code/extensions/node_modules/typescript/lib/lib.es5.d.ts
interface TemplateStringsArray extends ReadonlyArray<string> {
readonly raw: ReadonlyArray<string>;
}AND
- the usage of
tagFunction`<type_safe_accessor>`gives the following error, completely preventing the type-safe usage:
Argument of type '{}' is not assignable to parameter of type keyof TypeAndValueObject | TemplateStringsArray<keyof TypeAndValueObject>'.
Type '{}' is missing the following properties from type 'TemplateStringsArray<keyof TypeAndValueObject>': raw, length, concat, join, and 19 more. ts(2345)Examples
The working case
I have some type-safe i18n translations:
// Dictionary.ts
export interface Dictionary {
"Hello": string;
"Click to see more": string;
"Today is": (day: string) => string;
}
// en.ts
import { Dictionary } from "./Dictionary";
export const en: Dictionary = {
"Hello": "Hello",
"Click to see more": "Click to see more",
"Today is": (day: string) => `Today is ${day}`,
};
// lt.ts
import { Dictionary } from "./Dictionary";
export const lt: Dictionary = {
"Hello": "Sveiki",
"Click to see more": "Paspauskite, kad pamatytumėte daugiau",
"Today is": (day: string) => `Šiandien yra ${day}`,
};// i18n.ts
import { Dictionary } from "./Dictionary";
import { en } from "./en";
import { lt } from "./lt";
export interface ITranslations {
en: Dictionary;
lt: Dictionary;
}
export const translations: ITranslations = {
en: en,
lt: lt,
};
// "en" | "lt"
export type ILang = keyof ITranslations;I have a function to get the translations:
import { ILang, translations } from "./i18n";
import { Dictionary } from "./Dictionary";
const currentLang: ILang = "en";
const dictionary: Dictionary = translations[currentLang];
export const selectTranslation = <K extends keyof Dictionary>(key: K): Dictionary[K] => {
const translationText: Dictionary[K] = dictionary[key];
return translationText;
};And I can safely use it with type-safe translations in the following fashion:
// someFile.ts
import { selectTranslation } from "../selectTranslation.ts";
/**
* Type-checks, auto-completes etc. the strings from the `Dictionary`
*/
const someText: string = selectTranslation("Hello");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 selectTranslation function:
// selectTranslation.ts
import { ILang, translations } from "./i18n";
import { Dictionary } from "./Dictionary";
const currentLang: ILang = "en";
const dictionary: Dictionary = translations[currentLang];
export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringArray): Dictionary[K] => {
let realKey: K;
if (Array.isArray(key)) {
realKey = key[0];
} else {
realKey = key; // error:
/**
* Type 'K | TemplateStringsArray<string>' is not assignable to type 'K'.
* Type 'TemplateStringsArray<string>' is not assignable to type 'K'. ts(2322)
*/
}
const translationText: Dictionary[K] = dictionary[realKey];
return translationText;
};// anotherFile.ts
import { selectTranslation } from "../selectTranslation.ts";
/**
* Does NOT type-check and gives the previously mentioned error:
*
* Argument of type '{}' is not assignable to parameter of type K | TemplateStringsArray<K>'.
* Type '{}' is missing the following properties from type 'TemplateStringsArray<K>': raw, length, concat, join, and 19 more. ts(2345)
*/
const someText: string = selectTranslation`Hello`; // errorPossible solutions
I've tried 3 different solutions, neither of them giving me the results that I want:
a) Change the TemplateStringsArray to be a generic like so
// source code:
// TypeScript/src/lib/es5.d.ts
// TypeScript/lib/lib.es5.d.ts
// local install:
// /usr/lib/code/extensions/node_modules/typescript/lib/lib.es5.d.ts
interface TemplateStringsArray<T = string> extends ReadonlyArray<T> {
readonly raw: ReadonlyArray<T>;
}and used it like so
// selectTranslation.ts
-export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringsArray): Dictionary[K] => {
+export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringsArray<K>): Dictionary[K] => {however, the same problems persisted - the casting from key to realKey failed
AND the tagged function usage still failed
// selectTranslation.ts
if (Array.isArray(key)) {
realKey = key[0];
} else {
realKey = key; // still errors
}
// anotherFile.ts
const someText: string = selectTranslation`Hello`; // still errorsb) Instead of using TemplateStringsArray, just use Array<K>
// selectTranslation.ts
-export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringsArray): Dictionary[K] => {
+export const selectTranslation = <K extends keyof Dictionary>(key: K | Array<K>): Dictionary[K] => {the first problem of casting from key to realKey disappeared,
BUT the second one still remained
// selectTranslation.ts
if (Array.isArray(key)) {
realKey = key[0];
} else {
realKey = key; // all cool now
}
// anotherFile.ts
const someText: string = selectTranslation`Hello`; // still errors!c) Just use any
// selectTranslation.ts
-export const selectTranslation = <K extends keyof Dictionary>(key: K | TemplateStringsArray): Dictionary[K] => {
+export const selectTranslation = <K extends keyof Dictionary>(key: K | any): Dictionary[K] => {which 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 selectTranslation function as a tag function with type safety.
Is there any way to make it possible?
Reminder - we have 2 problems here:
- (less important since it can be avoided by using
Array<K>)TemplateStringsArraydoes not work like it should (or I'm using it wrong), even when used as a generic - (more important) tag functions cannot have type-safe parameters?
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
I'm happy to help if you have any further questions.
Activity
RyanCavanaugh commentedon Sep 12, 2019
Daniel Rosenwasser (@DanielRosenwasser) any pre-discussion thoughts?
DanielRosenwasser commentedon Sep 12, 2019
Seems related to #16552 (maybe it's a duplicate).
DanielRosenwasser commentedon Sep 12, 2019
I 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. Maybebut that looks awful.
trusktr commentedon Sep 23, 2020
Now 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))
justinfagnani commentedon Sep 23, 2020
What would great to have here is for
TemplateStringsArrayto be parameterized by the tuple type of it's literals.So the type of strings in:
would be:
trusktr commentedon Sep 24, 2020
If we were to write
we'd want to be able to work with a tuple type something like
But how can a generic function work with a particular specific type if the type signature is generic?
So if we write something like
then the code we write in
foocan't do anything more specific withT.How would it work?
0kku commentedon Sep 24, 2020
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:
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.trusktr commentedon Sep 24, 2020
But 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:
(playground link)
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?trusktr commentedon Sep 24, 2020
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?trusktr commentedon Sep 24, 2020
Titian Cernicova-Dragomir (@dragomirtitian) added return types, so in
the type of
divwould be inferred toHTMLDivElement, andpwill beHTMLParagraphElement.playground example
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>`.voxpelli commentedon Oct 27, 2020
Duplicate of #31422?
trusktr commentedon Nov 20, 2020
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:
but this doesn't:
(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.
justinfagnani commentedon Jan 13, 2021
Now 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:
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.Anders Hejlsberg (@ahejlsberg)
imjamesb commentedon Mar 31, 2022
Is there any news on this?
stephenh commentedon Apr 10, 2022
Fwiw 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:This types
wordsas["foo" | "bar" | "zaz"][]. Neat!But imo it would be even more neat as a tagged template:
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.)reverofevil commentedon May 23, 2022
Tagged template literals should be typechecked as regular function calls.
Return type of this call is inferred as
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.
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?
wmertens commentedon Mar 13, 2023
Seems like this almost works but not quite? The below errors with
mindplay-dk commentedon Jul 17, 2023
It looks like template string types are somehow "lossy"?
playground 👈
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.)
DerekZiemba commentedon Dec 10, 2024
This has bugged me for a while. I'm faced with two options:
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.
martaver commentedon Mar 27, 2025
So 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?
justinfagnani commentedon May 28, 2025
Another 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:
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:
use const-known values for strings in tagged template literals
char commentedon Jan 27, 2026
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.