Open xialvjun opened 5 years ago
+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.
It'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" }}`)
@RyanCavanaugh I see you added the "awaiting more feedback" label to this issue, so here I am 🤓
I completely agree with @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 e
would reflect the narrowest type of the first element ("some text"
) in tags
).
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.
This would be largely beneficial for SDL library developers. What's blocking the implementation of this narrowing?
I 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 that the 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()}`;
@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:
const QUERY = gql`query Foo { ... }`; query(QUERY)
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 gql
that looks something like gql(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.
@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.
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, QUERY
has type any
, and to use it I have to write something like this:
import { OrderQuery } from "./__generated__/OrderQuery"
const orders = useQuery<OrderQuery>(QUERY)
If the OrderQuery
query 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.
I'd like to be able to change the codegen process, so instead of it generating the __generated__/OrderQuery.ts
file 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.ts
file which the codegen would place in the appropriate location so that tsc automatically picks it up, so it'd cause QUERY
to have the appropriate type without having to change anything in the original file.
Then, useQuery
could be defined as something like useQuery<TVariables, TData>(query: GraphQLQuery<TVariables, TData>): GraphQLResult<TVariables, TData>
. Then, in my calling code I could just type const orders = useQuery(QUERY)
and orders
would 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 :)
@excitedleigh that makes great sense! Thank you for elaborating on your idea for a DX which would be made possible.
I wanted to (hopefully) re-draw attention to this issue, as I do believe this slight type-safety enhancement could result in some extraordinary experiences.
With 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...
I 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?
@harrysolovay It is clearly working as intended, as TemplateStringsArray
has been added specifically, so this is about changing the intention, hence a feature request, not a bug.
@voxpelli, specifying string[]
in place of TemplateStringsArray
should not make any difference to the runtime. It currently seems to be a crutch of sorts, helping the type-checker. I'd argue that TemplateStringsArray
is 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[]
, since string[]
is mutable, and TemplateStringsArray
is immutable. TemplateStringsArray
is also needed for the raw
property.
https://github.com/microsoft/TypeScript/issues/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
Dear 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!
+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 graphqlTag
doesn’t work.
The htm library could also use this functionality to enforce types within its hyperscript DSL.
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,
QUERY
has typeany
, and to use it I have to write something like this:import { OrderQuery } from "./__generated__/OrderQuery" const orders = useQuery<OrderQuery>(QUERY)
If the
OrderQuery
query 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.ts
file 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.ts
file which the codegen would place in the appropriate location so that tsc automatically picks it up, so it'd causeQUERY
to have the appropriate type without having to change anything in the original file.Then,
useQuery
could 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)
andorders
would 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 of gql`{ query }`
I'm surprised that this issue has stagnated. Any updates?
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. https://github.com/microsoft/TypeScript/issues/16552#issuecomment-492919476
Besides,
TemplateStringsArray
isReadonlyArray<string>
, it should be const anyway.