Type safety with `TemplateStringsArray` and tag functions
See original GitHub issueSearch 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
TemplateStringsArray
is 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`; // error
Possible 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 errors
b) 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>
)TemplateStringsArray
does 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.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:83
- Comments:16 (3 by maintainers)
@DanielRosenwasser @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.
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 @0kku)