microsoft / microsoft/TypeScript
Type safety with `TemplateStringsArray` and tag functions
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 111k
- Forks
- 14.3k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 132
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`; // 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>)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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Review the TemplateStringsArray declarations in TypeScript/src/lib/es5.d.ts and TypeScript/lib/lib.es5.d.ts, then reproduce the tagged call in the supplied selectTranslation example. Determine whether the requested generic array and type-safe tag-function behavior can be specified without breaking existing typing; done means an accepted design with corresponding type-checking coverage.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100