microsoft / microsoft/TypeScript

Type safety with `TemplateStringsArray` and tag functions

Offen
#33,304 22 Kommentare 213 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

In Discussion Suggestion
Vorherrschende Sprache
Go
Sterne
111k
Forks
14.3k
Ø Merge
1 T. 19 Std.
Gemergte PRs (30 T.)
117

Beschreibung

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

  1. 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

  1. 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:

  1. (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
  2. (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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Überprüfe die TemplateStringsArray-Deklarationen in TypeScript/src/lib/es5.d.ts und TypeScript/lib/lib.es5.d.ts und reproduziere anschließend den getaggten Aufruf im bereitgestellten selectTranslation-Beispiel. Ermittle, ob das angeforderte generische Array und das typsichere Verhalten der Tag-Funktion spezifiziert werden können, ohne die bestehende Typisierung zu beeinträchtigen; abgeschlossen ist die Aufgabe mit einem akzeptierten Entwurf und entsprechender Abdeckung durch Typprüfungen.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
typescript
Bereich
compilers
Issue-Typ
Feature
Schwierigkeit
5/5
Geschätzter Aufwand
Über eine Woche
Aktivitätsstatus
Veraltet
Klarheit
Muss geklärt werden
Anfängerfreundlichkeit
25/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.