Repository navigation
const TemplateStringsArray for TaggedTemplateExpression #31422
Description
Activity
- addedAwaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScriptAn idea for TypeScript
on May 17, 2019 +1 on this. This (superficially, at least) seems like a very small change that could have major benefits. It would be really useful to be able to construct simple DSLs using tagged template strings with types that could vary depending on the literal string arguments. A trivial example:
type Meters = Brand<number, 'meters'>; function withUnits(literals: [' + '], a: Meters, b: Meters): Meters; function withUnits(literals: [' - '], a: Meters, b: Meters): Meters; function withUnits(literals: [' * '], a: number, b: Meters): Meters; function withUnits(literals: [' * '], a: Meters, b: number): Meters; function withUnits(literals: [' / '], a: Meters, b: Meters): number; function withUnits(literals: [' + ' | ' - ' | ' * ' | ' / '], a: any, b: any): any { switch (literals[0]) { case ' + ': return a + b; case ' - ': return a - b; case ' * ': return a * b; case ' / ': return a / b; } } const a = 100 as Meters; const b = 200 as Meters; const foo: Meters = withUnits`${a} + ${b}`; const bar: number = withUnits`${a} / ${b}`;
This doesn't constitute a solution to #364, but a more sophisticated version of the above might make for a more convenient way to use type-safe units in TypeScript. That's just one example of how enabling this kind of simple type-safe DSL may be useful.
Reacted by Harry Solovay, ExE Boss, Pelle Wessman, Brian Kim, mx-rd, bgenia and EliReacted by Harry SolovayReacted by Harry SolovayIt's been a while without activity on this, I am generating localization strings from a JSON file and it would be really helpful in those cases too
type DictEntry = "greetings.hello" | "actions.success" interface Props { name: string } const translate = ([str]: TemplateStringsArray, obj: Props) => { } console.log(translate`greetings.hello ${{ name: "Daniell" }}`)
Reacted by Harry Solovay and Jay HarrisReacted by Harry SolovayReacted by Harry SolovayRyan Cavanaugh (@RyanCavanaugh) I see you added the "awaiting more feedback" label to this issue, so here I am 🤓
I completely agree with xialvjun (@xialvjun)'s suggestion
Let's say I want to create a tag which returns an object containing a prop, which is drawn from the first string of the
TemplateStringsArray.declare function createObjFromFirstTag<T extends TemplateStringsArray>( tags: T, ): { e: T[0]; };
One would think that
ewould reflect the narrowest type of the first element ("some text") intags).createObjFromFirstTag`some text`;
Expected Type
{e: "some text"}
Actual Type
{e: string}
With variadic tuple type support coming in 4.0 (🥳), it––imho––seems like this suggestion is also fit for the release. Tagged templates are such a powerful tool for library developers. It could be very useful to the end developer to enable type-checking of supplied values as literals.
Reacted by Daniel Shaffer, Pelle Wessman and Jay HarrisThis would be largely beneficial for SDL library developers. What's blocking the implementation of this narrowing?
Reacted by Harry SolovayI would be extremely appreciative of any more thoughts from the community & TS team! Seems like a very valuable feature.
I see many people just focus on
TemplateStringsArray should be a const tuple type, but I want to emphasize thatthe rest values is also a tuple type:let s: SQL<['select * from person where a=', ' and b=', ''], [number, Date]> = sql`select * from person where a=${1} and b=${new Date()}`;Reacted by Harry Solovay, ExE Boss and Jay HarrisReacted by Harry Solovayxialvjun (@xialvjun) I 100% agree. Having the types narrowed paves the way for extraordinary typescript-based DSL experiences.
I was originally thinking this would enable cool JSX alternatives.
const MyComponent = Tree(_ => _ _`div`( _`span`("Hello World"), ), propsTree); // can be type-checked according to valid props for the given element (specified above within tags) const propsTree = { className: "container", children: [ { className: "green", }, ], } as const;
Now I'm imagining the implications for the GraphQL use case:
const schema = Schema(_ => _ .scalar<Date>`Date` .type`User`( _`name``String`, _`dob``Date` ) .type`Query`( _`users``List``User`, ) );
From the schema definition above, one could create type-safe resolver implementations and document builders, all from a single schema definition.
const impl = schema.impl<Ctx>({ query: { users: (_parent, _args, _ctx) => {/**/} } }); const document = schema.doc(_ => _.users(_ => _.name().dob()));
This experience would be quite nice! Especially in contrast with code-gen heavy, GQL-first approaches.
Just as another, related example of a use case: the use case I'm thinking of has to do with making "code-gen heavy, GQL-first approaches" a little less verbose! At the moment, using those codegen solutions generally looks like this:
- Write something like
const QUERY = gql`query Foo { ... }`; query(QUERY) - Wait for the codegen to run
- Import some codegen-ed types, and replace the above with
const QUERY = gql`query Foo { ... }`; query<Foo, FooVariables>(QUERY)
With this feature, it'd be possible to just write the first line, and then codegen an overloaded type for
gqlthat looks something likegql(text: ['query Foo { ... }'], values: []): GraphQLQueryObject<Foo, FooVariables>. Then, the query has the right types without having to manually import them and specify them in the right place.Reacted by Harry Solovay and Max MakarovReacted by Harry Solovay and Max MakarovReacted by Harry Solovay and Max MakarovReacted by Harry Solovay and Max Makarov- Write something like
Daisy Leigh Brenecki (@excitedleigh) I'd love to hear more about your idea for a TS-first GQL experience, and how type-safe tagged template expressions would enable that experience.
A more detailed explanation of the use case could potentially help us get buy-in. Gearing up for the release of 4.0, I imagine that TS team members are dealing with quite a lot at the moment. Hopefully we can make a good-enough argument to get this fix/feature on the roadmap soon-after :)
Alright, I'll try to elaborate if I can.
The current state of affairs
At current, using something like Apollo with its TypeScript codegen tool, I'd write something like this:
const QUERY = gql` query OrderQuery { branches { id branchName } orders(first: 20) { id ...ExistingOrderItem_order branch { id } } } ${ExistingOrderItem.fragments.order} `
At current,
QUERYhas typeany, and to use it I have to write something like this:import { OrderQuery } from "./__generated__/OrderQuery" const orders = useQuery<OrderQuery>(QUERY)
If the
OrderQueryquery had taken variables as inputs, I'd instead have to write something like:import { OrderQuery, OrderQueryVariables } from "./__generated__/OrderQuery" const orders = useQuery<OrderQuery, OrderQueryVariables>(QUERY)
...which is even more verbose.
What I'd like to be able to build
I'd like to be able to change the codegen process, so instead of it generating the
__generated__/OrderQuery.tsfile which I then have to import it generates something along these lines:declare function gql(text: readonly [ "\n query OrderQuery {\n branches {\n id\n branchName\n }\n orders(first: 20) {\n id\n ...ExistingOrderItem_order\n branch {\n id\n }\n }\n }\n ", "\n", ], values: readonly [typeof ExistingOrderItem.fragments.order]): GraphQLQuery< { /* calculated type of variables goes here */ }, { /* calculated type of resulting data values goes here */}, >
That definition would go in a
.d.tsfile which the codegen would place in the appropriate location so that tsc automatically picks it up, so it'd causeQUERYto have the appropriate type without having to change anything in the original file.Then,
useQuerycould be defined as something likeuseQuery<TVariables, TData>(query: GraphQLQuery<TVariables, TData>): GraphQLResult<TVariables, TData>. Then, in my calling code I could just typeconst orders = useQuery(QUERY)andorderswould have the correct inferred type.Does that help?
PS: congrats on the impending 4.0 release! I'm especially excited about the tuple spread type stuff :)
Reacted by Harry Solovay, Tom Picton, Oliver Bell, Yaojian, Tobias Diez and Nicolas LepageDaisy Leigh Brenecki (@excitedleigh) that makes great sense! Thank you for elaborating on your idea for a DX which would be made possible.
Reacted by Daisy Leigh Brenecki, xialvjun and Max MakarovI wanted to (hopefully) re-draw attention to this issue, as I do believe this slight type-safety enhancement could result in some extraordinary experiences.
Reacted by xialvjun, Tom Picton and Max MakarovWith the template literal type features of 4.1, I believe this narrowing is more important than when this issue was first opened.
Tagged template library developers will want to...
- Protect their users from supplying incompatibly-typed arguments.
- Produce literal types which can be utilized elsewhere, outside of the tagged template.
- Eliminate the unnecessary right and left parens from their string-accepting fns.
Reacted by ExE Boss, Tom Picton, __, Stanley Stuart and Jay HarrisI just checked and saw that this issue is not labeled with "Bug". I believe this is a mistake. A tagged template is just a function. One can even call it as such.
declare function myTag<T extends TemplateStringsArray>(tags: T): T[0]; myTag(["first"]); // type: "first"
Why would tagged templates (mere functions) behave differently when it comes to narrowing argument types? This is inconsistent.
I'm curious whether others also feel that this issue merits the "Bug" label?
Reacted by Martin Johns, Pelle Wessman and ExE BossHarry Solovay (@harrysolovay) It is clearly working as intended, as
TemplateStringsArrayhas been added specifically, so this is about changing the intention, hence a feature request, not a bug.Pelle Wessman (@voxpelli), specifying
string[]in place ofTemplateStringsArrayshould not make any difference to the runtime. It currently seems to be a crutch of sorts, helping the type-checker. I'd argue thatTemplateStringsArrayis the wrong solution.I'd also urge you to communicate the "why?"
What do you mean by:
this is about changing the intention
Moreover...
It is clearly working as intended
This is actually not clear from your description.
I'd love to hear your thoughts more in depth!
Actually, you should use
readonly string[], sincestring[]is mutable, andTemplateStringsArrayis immutable.TemplateStringsArrayis also needed for therawproperty.Reacted by Harry Solovay#33304 is a similar issue, and in that one it shows how Template String types would allow for doing things like
const div = html`<div>...</div>` // div would have implicit type HTMLDivElement const p = html`<p>...</p>` // p would have implicit type HTMLParagraphElement
Reacted by ExE Boss, Kipras Melnikovas, a11delavar, celeste-ellerby and Jay HarrisDear TS team, I'd like to reiterate:
Tagged template library developers will want to...
- Protect their users from supplying incompatibly-typed arguments.
- Produce literal types which can be utilized elsewhere, outside of the tagged template.
- Eliminate the unnecessary right and left parens from their string-accepting fns.
Currently, the above is not possible.
Any thoughts would be greatly appreciated!
Reacted by ExE Boss, xialvjun, T6, Stanley Stuart, Oliver Bell, Pelle Wessman, Tobias Diez, Nicolas Lepage, Max Makarov, celeste-ellerby and 1 more+1 for this feature. I have a case where I’m writing typings for a Flow library, where we could have safer typings thanks to template literal types, except the pattern the library uses is to have a tagged template function. I can’t really change their pattern.
The example can be found here, where
graphqlTagdoesn’t work.Reacted by ExE Boss and Dotan SimhaReacted by Harry Solovay, Jan Kassens, Justin Bennett, Tim Griesser, ExE Boss, Tobias Diez and Dotan SimhaThe htm library could also use this functionality to enforce types within its hyperscript DSL.
Reacted by Harry Solovay, Daisy Leigh Brenecki, Dotan Simha, Jay Harris and EliAlright, I'll try to elaborate if I can.
The current state of affairs
At current, using something like Apollo with its TypeScript codegen tool, I'd write something like this:
const QUERY = gql` query OrderQuery { branches { id branchName } orders(first: 20) { id ...ExistingOrderItem_order branch { id } } } ${ExistingOrderItem.fragments.order} `
At current,
QUERYhas typeany, and to use it I have to write something like this:import { OrderQuery } from "./__generated__/OrderQuery" const orders = useQuery<OrderQuery>(QUERY)
If the
OrderQueryquery had taken variables as inputs, I'd instead have to write something like:import { OrderQuery, OrderQueryVariables } from "./__generated__/OrderQuery" const orders = useQuery<OrderQuery, OrderQueryVariables>(QUERY)
...which is even more verbose.
What I'd like to be able to build
I'd like to be able to change the codegen process, so instead of it generating the
__generated__/OrderQuery.tsfile which I then have to import it generates something along these lines:declare function gql(text: readonly [ "\n query OrderQuery {\n branches {\n id\n branchName\n }\n orders(first: 20) {\n id\n ...ExistingOrderItem_order\n branch {\n id\n }\n }\n }\n ", "\n", ], values: readonly [typeof ExistingOrderItem.fragments.order]): GraphQLQuery< { /* calculated type of variables goes here */ }, { /* calculated type of resulting data values goes here */}, >
That definition would go in a
.d.tsfile which the codegen would place in the appropriate location so that tsc automatically picks it up, so it'd causeQUERYto have the appropriate type without having to change anything in the original file.Then,
useQuerycould be defined as something likeuseQuery<TVariables, TData>(query: GraphQLQuery<TVariables, TData>): GraphQLResult<TVariables, TData>. Then, in my calling code I could just typeconst orders = useQuery(QUERY)andorderswould have the correct inferred type.Does that help?
PS: congrats on the impending 4.0 release! I'm especially excited about the tuple spread type stuff :)
We were able to build something similar using graphql-code-generator: https://www.graphql-code-generator.com/plugins/gql-tag-operations-preset
However, currently, it requires using
gql(`{ query }`)instead ofgql`{ query }`Reacted by ExE Boss and LukeReacted by ExE BossI'm surprised that this issue has stagnated. Any updates?
Reacted by xialvjun, Arthur Bodera, Olle Bröms, Camron Flanders, Jay Harris and Tomasz Robaczewski
Search Terms
TemplateStringsArray, TaggedTemplateExpression
Suggestion
There shouldn't be a type
TemplateStringsArray, it should be aconst string tuple type, so we can write code:Use Cases
Just like in the above code, we can test the sql at compile time or use a TypeScript Language Service Plugin (in fact, I'm writing itts-sql-plugin and then come across this problem).
Examples
Checklist
My suggestion meets these guidelines:
There is another issue is alike. #16552 (comment)
Besides,
TemplateStringsArrayisReadonlyArray<string>, it should be const anyway.