diff --git a/__tests__/integration/query-typings.test.ts b/__tests__/integration/query-typings.test.ts index fd9c9021..b728e665 100644 --- a/__tests__/integration/query-typings.test.ts +++ b/__tests__/integration/query-typings.test.ts @@ -20,7 +20,6 @@ describe.each` const paginatedQuery = fql`[{ "x": 123}].toSet()`; if ("query" === method) { - expect.assertions(1); const result = (await client.query(query)).data; expect(result).toEqual({ x: 123 }); } else { @@ -43,23 +42,16 @@ describe.each` }); it("allows customers to infer their own types in queries from fql statements", async () => { - // This is a noop function that is only used to validate the inferred type of the query - // It will fail at build time if types are not inferred correctly. - const noopToValidateInferredType = (value: MyType) => {}; - const query = fql`{ "x": 123 }`; const paginatedQuery = fql>`[{ "x": 123}].toSet()`; if ("query" === method) { - expect.assertions(1); const result = (await client.query(query)).data; - noopToValidateInferredType(result); expect(result).toEqual({ x: 123 }); } else { expect.assertions(2); for await (const page of client.paginate(paginatedQuery)) { for (const result of page) { - noopToValidateInferredType(result); expect(result).toEqual({ x: 123 }); } } @@ -69,11 +61,16 @@ describe.each` // exactly one item is returned. for await (const page of client.paginate(query)) { for (const result of page) { - noopToValidateInferredType(result); expect(result).toEqual({ x: 123 }); } } } - Promise.resolve(); + }); + + it("allows customers to use subtyped queries", async () => { + const query = fql`"hello"`; + + const result = (await client.query(query)).data; + expect(result).toEqual("hello"); }); }); diff --git a/src/client.ts b/src/client.ts index 49afb879..7ca9b3cb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -211,7 +211,12 @@ export class Client { * ``` */ paginate( - iterable: Page | EmbeddedSet | Query> | Query, + iterable: + | Page + | EmbeddedSet + | Query> + | Query + | Query>, // needed for inference when using the `fql` function within `.paginate()`, e.g. client.paginate(fql`...`) options?: QueryOptions, ): SetIterator { if (iterable instanceof Query) { diff --git a/src/query-builder.ts b/src/query-builder.ts index bd5ba41b..8ae0e553 100644 --- a/src/query-builder.ts +++ b/src/query-builder.ts @@ -22,7 +22,7 @@ export type QueryArgumentObject = { */ export type QueryArgument = | QueryValue - | Query + | Query | Date | ArrayBuffer | Uint8Array @@ -46,7 +46,7 @@ export type QueryArgument = * const queryRequestBuilder = fql`${str}.length == ${innerQuery}`; * ``` */ -export function fql( +export function fql( queryFragments: ReadonlyArray, ...queryArgs: QueryArgument[] ): Query { @@ -61,13 +61,22 @@ export function fql( * T can be used to infer the type of the response type from {@link Client} * methods. */ -// HACK: We need to disable the next line because the type param is never -// explicitly used in the class. It is inferred by the constructor and -// used as the return type. You cannot annotate the constructor return type. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export class Query { +export class Query { readonly #queryFragments: ReadonlyArray; readonly #interpolatedArgs: QueryArgument[]; + /** + * A phantom field to enforce the type of the Query. + * @internal + * + * We need to provide an actual property of type `T` for Typescript to + * actually enforce it. + * + * "Because TypeScript is a structural type system, type parameters only + * affect the resulting type when consumed as part of the type of a member." + * + * @see {@link https://www.typescriptlang.org/docs/handbook/type-compatibility.html#generics} + */ + readonly #__phantom: T; constructor( queryFragments: ReadonlyArray, @@ -81,6 +90,9 @@ export class Query { } this.#queryFragments = queryFragments; this.#interpolatedArgs = queryArgs; + + // HACK: We have to construct the phantom field, but we don't have any value for it. + this.#__phantom = undefined as unknown as T; } /**