Skip to content
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

const TemplateStringsArray for TaggedTemplateExpression #31422

Open
5 tasks done
xialvjun opened this issue May 16, 2019 · 23 comments · May be fixed by #49552
Open
5 tasks done

const TemplateStringsArray for TaggedTemplateExpression #31422

xialvjun opened this issue May 16, 2019 · 23 comments · May be fixed by #49552
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@xialvjun
Copy link

xialvjun commented May 16, 2019

Search Terms

TemplateStringsArray, TaggedTemplateExpression

Suggestion

There shouldn't be a type TemplateStringsArray, it should be a const string tuple type, so we can write code:

interface SQL<TSA, VS> {
  texts: TSA;
  values: VS;
}

function sql<TSA extends readonly string[], VS extends any[]>(texts:TSA, ...values: VS): SQL<TSA, VS> {
  return { texts, values };
}

// then
let s: SQL<['select * from person where a=', ' and b=', ''], [number, Date]> = sql`select * from person where a=${1} and b=${new Date()}`;

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:

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

There is another issue is alike. #16552 (comment)

Besides, TemplateStringsArray is ReadonlyArray<string>, it should be const anyway.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels May 17, 2019
@sethfowler
Copy link

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

@daniellwdb
Copy link

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" }}`)

@harrysolovay
Copy link

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

@davit-b
Copy link

davit-b commented Jul 6, 2020

This would be largely beneficial for SDL library developers. What's blocking the implementation of this narrowing?

@harrysolovay
Copy link

I would be extremely appreciative of any more thoughts from the community & TS team! Seems like a very valuable feature.

@xialvjun
Copy link
Author

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()}`;

@harrysolovay
Copy link

harrysolovay commented Jul 14, 2020

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

@daisylb
Copy link

daisylb commented Jul 15, 2020

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:

  1. Write something like const QUERY = gql`query Foo { ... }`; query(QUERY)
  2. Wait for the codegen to run
  3. 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 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.

@harrysolovay
Copy link

@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 :)

@daisylb
Copy link

daisylb commented Jul 16, 2020

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

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 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 :)

@harrysolovay
Copy link

@excitedleigh that makes great sense! Thank you for elaborating on your idea for a DX which would be made possible.

@harrysolovay
Copy link

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.

@harrysolovay
Copy link

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

  1. Protect their users from supplying incompatibly-typed arguments.
  2. Produce literal types which can be utilized elsewhere, outside of the tagged template.
  3. Eliminate the unnecessary right and left parens from their string-accepting fns.

@harrysolovay
Copy link

harrysolovay commented Nov 3, 2020

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?

@voxpelli
Copy link

voxpelli commented Nov 3, 2020

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

@harrysolovay
Copy link

@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!

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Nov 3, 2020

Actually, you should use readonly string[], since string[] is mutable, and TemplateStringsArray is immutable. TemplateStringsArray is also needed for the raw property.

Spec: https://tc39.es/ecma262/#sec-gettemplateobject

@trusktr
Copy link
Contributor

trusktr commented Nov 20, 2020

#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

@harrysolovay
Copy link

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!

@alloy
Copy link
Member

alloy commented Mar 24, 2021

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

@jugglinmike
Copy link

The htm library could also use this functionality to enforce types within its hyperscript DSL.

@n1ru4l
Copy link

n1ru4l commented Nov 25, 2021

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

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 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 :)

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 }`

@harrysolovay
Copy link

I'm surprised that this issue has stagnated. Any updates?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.