microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.06k stars 12.49k forks source link

const TemplateStringsArray for TaggedTemplateExpression #31422

Open xialvjun opened 5 years ago

xialvjun commented 5 years ago

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:

There is another issue is alike. https://github.com/microsoft/TypeScript/issues/16552#issuecomment-492919476

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

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

daniellwdb commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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

harrysolovay commented 4 years ago

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

xialvjun commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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

harrysolovay commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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 commented 3 years ago

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
harrysolovay commented 3 years ago

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 commented 3 years ago

+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 commented 3 years ago

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

n1ru4l commented 2 years ago

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 commented 2 years ago

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