-
Notifications
You must be signed in to change notification settings - Fork 13k
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
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 codeThis wouldn't change the runtime behavior of existing JavaScript codeThis could be implemented without emitting different JS based on the types of the expressionsThis 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.To pick up a draggable item, press the space bar. While dragging, use the arrow keys to move the item. Press space again to drop the item in its new position, or press escape to cancel.
I'm happy to help if you have any further questions.
Activity
RyanCavanaugh commentedon Sep 12, 2019
@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
TemplateStringsArray
s 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 @0kku)
justinfagnani commentedon Sep 23, 2020
What would great to have here is for
TemplateStringsArray
to 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
foo
can'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
T
asTemplateStringsArrayOf<["<div class='", "'>", "</div>"]>
andK
as[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
T
will be, in order to type check it in its implementation. So even ifT
will 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
html
call, the type ofT
is 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
asdf
in 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 theis
operator, but my attempt failed. Can that be done?trusktr commentedon Sep 24, 2020
@dragomirtitian added return types, so in
the type of
div
would be inferred toHTMLDivElement
, andp
will 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
@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.
41 remaining items