sanity-io / sanity

Sanity Studio – Rapidly configure content workspaces powered by structured content
https://www.sanity.io
MIT License
5.27k stars 426 forks source link

Automatically infer types of queries using groq and @sanity/codegen #6934

Closed romeovs closed 4 months ago

romeovs commented 4 months ago

Is your feature request related to a problem? Please describe. Currently, I'm using groq tagged templates in my codebase in tandem with sanity typegen generate to generate types. This works well.

const example = groq`
  *[_type == 'post'] {
    title
    author-> {
      name
    }
  }
`

However, to make use of the generated type, I need to manually add the generated type annotations to client.fetch etc. to make use of them:

It would be nice if we could automatically type groq queries without having to manually add the generated type annotations:

import { ExampleQueryResult } from "./generated"
const res = await client.fetch<ExampleQueryResult>(example)

It would be nice if we could automatically type the query variable with the expected result and variable types using a phantom type like so:

type TypedQuery<Res> = string & {
  _result: ExampleQueryResult
}

const example: TypedQuery<ExampleQueryResult>

that way we can automatically infer the type of the query and we don't need the manual type annotation:

function inferredQuery<T>(query: TypedQuery<T>): Promise<T> {
  return client.fetch(query)
}

and we can just use it like so:

// res is typed as ExampleQueryResult here
const res = await inferredQuery(example)

Note: I've left out query parameters here for simplicity, but it would be easy to add them to TypedQuery

To facilitate this, we would need a couple of things.

1. Introduce a TypedQuery type

Introduce a new type TypedQuery that describes a query and with metadata like the return type and variables:

type TypedQuery<R, P> = string & {
  _result?: R
  _params?: P
}

This is a phantom type so does not impact runtime performance.

2. @sanity/codegen should record a map of all query strings

@sanity/codegen should generated a map that uses query strings as keys and has TypedQuery as values. Eg. in the example given above the map would look like:

// the generated type for the query result, this functionality already exists:
export type ExampleQueryResult = {
  title: string | null
  author: {
    name: string | null
  } | null
} | null

export type QueryTypes = {
  "\\n  *[_type == 'post'] {\\n    title\\n    author-> {\\n      name\\n    }\\n  }\\n":
    TypedQuery<ExampleQueryResult>
}

3. Return a TypedQuery from the groq wrapper

Using the type map, is now possible to return a TypedQuery from groq.

Unfortunately [TypeScript currently does not support using template strings as constant type]( parameters to template tags](https://github.com/microsoft/TypeScript/issues/33304), so for this step to work groq should be used as a function:

function groq<T extends keyof QueryTypes>(query: T): QueryTypes[T] {
  return query
}

We would then have to define the query as:

const example = groq(`
  *[_type == 'post'] {
    title
    author-> {
      name
    }
  }
`)

4. Support TypedQuery in client.fetch and similar functions

Add an overload to client.fetch and similar functions that automatically infers the result typed when a TypeQuery is passed, instead of just a query string:

function inferredQuery<T>(query: TypedQuery<T>): Promise<T> {
  return client.fetch(query)
}

Describe the solution you'd like All of the above steps can be done as separate utility functions and wrappers, except for 3. because @sanity/codegen does not detect a query if it is defined using groq(`...`) as opposed to groq`...`.

What would be need is for findQueriesInSource to parse groq queries when they are defined in the functional style (groq(`...`)).

Additional context

This functionality matches what is in graphql-codegen when using it with the client-preset and this would mimic it.

I would be happy to provide a PR or a POC repo where all of this is working together, but I wanted to check if there is interest in taking @sanity/codegen in this direction first.

sgulseth commented 4 months ago

Hi! Thanks for the request!

We've been discussing a similar approach ourselves internally, however with a bit different API. We would like the groq package to be independent in itself to the typegen module, so what we've been thinking about is adding a defineQuery(query: string): string-export to groq(For the same reasons with tagged template literals as you mention). Something similar to have I've outlined here, though it requires some changes to the sanity client as well before the playground is happy. We don't have any timeline on it atm, but any contribution would be appreciated