-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type safety with TemplateStringsArray
and tag functions
#33304
Comments
@DanielRosenwasser any pre-discussion thoughts? |
Seems related to #16552 (maybe it's a duplicate). |
I think the big problem is that you sort of want to create something that combines
but that looks awful. |
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) |
What would great to have here is for So the type of strings in: html`<div>${'a'}</div>` would be: TemplateStringsArray<['<div>', '</div>']> |
If we were to write foo`one\\n${1}two${2}three` we'd want to be able to work with a tuple type something like type TheStrings = readonly ['one\n', 'two', 'three'] & {
raw: readonly ['one\\n', 'two', 'three']
} But how can a generic function work with a particular specific type if the type signature is generic? So if we write something like function foo<T extends string[]>(strings: T) {} then the code we write in How would it work? |
type TemplateStrignsArrayOf<T extends ReadonlyArray<string>> = TemplateStringsArray & T; 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: declare function xml<
T extends TemplateStringsArray,
K extends ReadonlyArray<unknown>,
> (
strings: T,
...slots: K,
): baz;
xml`
<div class='${foo}'>${bar}</div>
`; to have it infer |
But the issue with that approach is (unless I missed something) that the code can not forecast what type Here's one way I think it could work: When Example: type AttributeNames = 'foo' | 'bar' | 'baz'
type Attribute<S extends string> = `${AttributeNames}="${S}"`
type TagNames = 'div' | 'p' | 'span'
type OpenTag<T extends TagNames, S extends string = ""> =
S extends "" ? `<${T}>` : `<${T} ${Attribute<S>}>`
type CloseTag<T extends TagNames> = `</${T}>`
type FullTag<T extends TagNames, S extends string = ""> = `${OpenTag<T, S>}${CloseTag<T>}`
declare function html<T extends FullTag<TagNames, 'asdf'>[]>(
strings: T,
...values: any[]
): any // When TypeScript sees html`asdf`, TypeScript will call our function like the following:
html([`asdf`]) // ERROR (good)
// When TypeScript sees html`<div foo="asdf"></div>`, TypeScript will call our function like the following:
html([`<div foo="asdf"></div>`]) // NO ERROR (good) where in each That example is also fairly simple. How can we allow any attribute value, instead of hard coding particular attribute values like |
I tried to make the template more generic with |
@dragomirtitian added return types, so in const div = html`<div>...</div>`
const p = html`<p>...</p>` the type of What I mean by "would be" is that currently, it works only if we call |
Duplicate of #31422? |
@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: const div = html(['<div>...</div>']) // div has implicit type HTMLDivElement, yaaay!
const p = html(['<p>...</p>']) // p has implicit type HTMLParagraphElement, yaaay! but this doesn't: const div = html`<div>...</div>` // type error, ... is not assignable to TemplateStringsArray
const p = html`<p>...</p>` // type error, ... is not assignable to TemplateStringsArray (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 #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: export const html = <T extends TemplateStringsArray<S>, S extends TemplateLiteral>(
strings: T,
...values: HTMLTemplateTypes<S>) => {
// ...
} I think we would need a type like |
Is there any news on this? |
Fwiw @DanielRosenwasser the "not a full GraphQL parser" :-) use case that I'd like to use a type-literal-aware Here's a working example with a non- type ToWords<S extends string> =
S extends `${infer H} ${infer T}`
? H | `${ToWords<T>}`
: S;
function w<S extends string>(strings: S): ToWords<S>[] {
return strings.split(" ") as ToWords<S>[];
}
const words = w(`foo bar zaz`); This types 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 |
Tagged template literals should be typechecked as regular function calls. declare const tag: <T extends readonly any[], U extends any[]>(t: T, ...u: U) => [T, U];
// code
tag`1${2}3\u${'4'}`;
// typechecked as if
declare const literals: readonly ['1', undefined] & {raw: readonly ['1', '3\\u']};
declare const args: [2, '4'];
tag(literals, ...args); Return type of this call is inferred as [
readonly ["1", undefined] & {raw: readonly ['1', '3\\u']},
[2, '4']
] 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. for (const {...} of query`SELECT * FROM widgets WHERE id = ${id}`) { ... } 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 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 @DanielRosenwasser Could you please reconsider it on the next design meeting? |
Seems like this almost works but not quite? The below errors with
import * as types from './graphql.js'
import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'
const documents = {
'\n query Foo {\n Tweets {\n id\n }\n }\n': types.FooDocument,
}
export function gql(source: string): unknown
export function gql(
source: '\n query Foo {\n Tweets {\n id\n }\n }\n'
): typeof documents['\n query Foo {\n Tweets {\n id\n }\n }\n']
// Try to define a static TemplateStringsArray
export function gql(
tmpl: readonly ['\n query Foo {\n Tweets {\n id\n }\n }\n']
& {raw: readonly ['\n query Foo {\n Tweets {\n id\n }\n }\n']}
): typeof documents['\n fragment Lel on Tweet {\n id\n body\n }\n']
export function gql(source: string | TemplateStringsArray) {
return (documents as any)[
typeof source ==='string' ? source : source[0]
] ?? {}
}
// works
const t = gql('\n query Foo {\n Tweets {\n id\n }\n }\n')
// fails
const t2 = gql`\n query Foo {\n Tweets {\n id\n }\n }\n` |
It looks like template string types are somehow "lossy"? Constant strings and numbers are widened to just strings and numbers, and constant string fragments are flattened to My hope is we would be able to parse SQL template literals at compile-time, so that e.g. Someone attempted to start a project here, but it works with string literals only - but imagine the awesome simplicity and power of something like (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.) |
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 I'm using JSDoc & plain JS as I find TS often just gets in the way. /**
* @template {string} T - The string to be trimmed.
* @template {string} [C=' '|'\t'|'\n'] - The characters to be trimmed from the start of the string. Defaults to space, tab, or newline.
* @typedef {T extends `${C}${`${C}${C}${C}`|`${C}`|''}${infer X}` ? TrimStart<X, C>: T} TrimStart
*/
/**
* @template {string} T - The string to be trimmed.
* @template {string} [C=' '|'\t'|'\n'] - The characters to be trimmed from the start of the string. Defaults to space, tab, or newline.
* @typedef {T extends `${infer X}${`${C}${C}${C}`|`${C}`|''}${C}` ? TrimEnd<X, C>: T} TrimEnd
*/
/**
* @template {string} T - The string to be trimmed.
* @template {string} [C=' '|'\t'|'\n'] - The characters to be trimmed from the start of the string. Defaults to space, tab, or newline.
* @typedef {TrimEnd<TrimStart<T, C>, C>} Trim
*/
/**
* @template {string} HTML
* @template {any} D - Default return value if fails to parse
* @typedef {Trim<HTML> extends infer S extends string ?
* S extends `<${infer K extends keyof HTMLElementTagNameMap} ${string}` ? HTMLElementTagNameMap[K] :
* S extends `${string}</${infer K extends keyof HTMLElementTagNameMap}>` ? HTMLElementTagNameMap[K] :
* S extends `<${infer K extends keyof HTMLElementTagNameMap}>` ? HTMLElementTagNameMap[K]
* : D
* : D} ParseHTMLElementType
*/
/**
* @type {{
* <VALUES extends readonly [(abstract new (...args: any) => unknown) | typeof Array | typeof HTMLCollection | typeof String, ...any[]]
* >(raw: TemplateStringsArray, ...values: VALUES):
* VALUES extends [(abstract new (...args: any) => infer V), ...infer _] ? V
* : VALUES extends [infer V extends Element | Element[] | HTMLCollectionOf<Element>, ...infer _] ? V
* : DocumentFragment;
* <TEMPLATE extends string | string[]>(raw: TEMPLATE):
* ParseHTMLElementType<
* Trim<TEMPLATE extends string
* ? TEMPLATE
* : TEMPLATE extends readonly string[]
* ? import('../types/ts-toolbelt/sources/String/Join').Join<TEMPLATE>
* : never
* >, void> extends infer E extends HTMLElement ? E
* : DocumentFragment;
* <TEMPLATE extends string | string[], VALUES extends readonly string[]>(raw: TEMPLATE, ...values: VALUES):
* ParseHTMLElementType<
* Trim<TEMPLATE extends string
* ? TEMPLATE
* : TEMPLATE extends readonly string[]
* ? import('../types/ts-toolbelt/sources/String/Join').Join<TEMPLATE>
* : never
* >, void> extends infer E extends HTMLElement ? E
* : VALUES extends [(abstract new (...args: any) => infer V), ...infer _] ? V
* : VALUES extends [infer V extends Element | Element[] | HTMLCollectionOf<Element>, ...infer _] ? V
* : DocumentFragment;
* }}
*/ // @ts-ignore
var html = function html(raw, ...values) {
let specificType;
if (raw.length > 0 && raw[0] === '' && values[0] != null) {
if (values[0].prototype instanceof Element || values[0] === Array || values[0] === HTMLCollection) {
specificType = values[0];
raw = raw.slice(1);
values.shift();
} else if (values[0] === String) {
values.shift();// @ts-ignore
return String.raw({ raw: raw.slice(1) }, ...values);
}
}
const template = document.createElement('template');
template.innerHTML = String.raw({ raw }, ...values);
const fragment = template.content;
if (specificType === HTMLTemplateElement) { // @ts-ignore
return template;
}
if (specificType === HTMLCollection) { // @ts-ignore
return fragment.children;
}
if (specificType === Array) { // @ts-ignore
return Array.from(fragment.children);
}
if (specificType === DocumentFragment) { // @ts-ignore
return fragment;
}
if (fragment.childElementCount === 1) { // @ts-ignore
return fragment.firstElementChild;
}// @ts-ignore
return fragment;
}; |
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 theCurrently, there's no way to do this, since
TemplateStringsArray
is not genericAND
gives the following error, completely preventing the type-safe usage:
Examples
The working case
I have some type-safe
i18n
translations:I have a function to get the translations:
And I can safely use it with type-safe translations in the following fashion:
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: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 soand used it like so
however, the same problems persisted - the casting from
key
torealKey
failedAND the tagged function usage still failed
b) Instead of using
TemplateStringsArray
, just useArray<K>
the first problem of casting from
key
torealKey
disappeared,BUT the second one still remained
c) Just use
any
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:
Array<K>
)TemplateStringsArray
does not work like it should (or I'm using it wrong), even when used as a genericChecklist
My suggestion meets these guidelines:
I'm happy to help if you have any further questions.
The text was updated successfully, but these errors were encountered: