diff --git a/README.md b/README.md index cad8b5c..93aba3e 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ const allUsers = await query('users').select((user) => [ ]), ]); -/** +/* * The type of `post` is inferred as * { * title: string; @@ -258,6 +258,59 @@ The `subscribe` method accepts three parameters: - onError: Optional. The error handler. It will be called when an error is emitted by the subscription. It accepts one parameter, which is the error emitted by the subscription. - onComplete: Optional. The completion handler. It will be called when the subscription is completed. It accepts no parameter. +### Infer TypeScript types from GraphQL schema + +It is annoying to define a type in GraphQL schema and repeat the same type in TypeScript. graphql-intuitive-request provides a convenient way to infer TypeScript types from the GraphQL schema. + +`schema` is a helper function to validate a GraphQL schema. It simply returns the schema itself and does nothing at runtime, but it is useful for TypeScript to ensure that the schema is valid. + +```typescript +import { createClient, enumOf, infer, schema } from 'graphql-intuitive-request'; + +const $ = schema({ + User: { + id: 'Int!', + username: 'String!', + email: 'String', + }, + Post: { + id: 'Int!', + status: 'PostStatus!', + title: 'String!', + content: 'String!', + author: 'User!', + }, + PostStatus: enumOf('DRAFT', 'PUBLISHED', 'ARCHIVED'), + + Query: { + users: [{}, '[User!]!'], + post: [{ id: 'Int!' }, 'Post'], + }, +}); + +export const client = createClient('https://example.com/graphql').withSchema($); +``` + +After that, you can use the `infer` helper function to infer TypeScript types from the schema. + +```typescript +const $$ = infer($); + +/* + * The type of `Post` is inferred as + * Array<{ + * id: number; + * status: "DRAFT" | "PUBLISHED" | "ARCHIVED"; + * title: string; + * content: string; + * author: { id: number; username: string; email: string | null }; + * }> + */ +export type Post = typeof $$.Post; +``` + +As you can see, you can use `typeof inferred.` to get the inferred TypeScript type of a type in the schema. Like `schema`, `infer` also does nothing at runtime, it is only used to infer TypeScript types from the schema. + ### Create object selector to eliminate duplication It is annoying to repeat the same object selector every time when you want to select some fields of an object. graphql-intuitive-request provides a convenient way to eliminate the duplication of object selector. diff --git a/package.json b/package.json index f4a3a1a..0c25775 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-intuitive-request", - "version": "0.1.3-dev", + "version": "0.1.3", "private": true, "description": "Intuitive and (more importantly) TS-friendly GraphQL client for queries, mutations and subscriptions", "homepage": "https://github.com/Snowfly-T/graphql-intuitive-request", diff --git a/src/index.ts b/src/index.ts index d98365f..e94ac7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ export { createClient } from './client'; export { queryString, mutationString, subscriptionString } from './query-builder'; export { selectorBuilder } from './selector'; -export { schema, enumOf } from './types'; +export { infer, enumOf, schema } from './types'; /** * Error thrown when the client receives an error from the server. diff --git a/src/types.proof.ts b/src/types.proof.ts new file mode 100644 index 0000000..f5f9876 --- /dev/null +++ b/src/types.proof.ts @@ -0,0 +1,79 @@ +import { describe, equal, expect, it } from 'typroof'; + +import { enumOf, infer, schema } from './types'; + +import type { GraphQLEnum } from './types/graphql-types'; + +let $: { + User: { + id: 'Int!'; + username: 'String!'; + email: 'String'; + }; + Post: { + id: 'Int!'; + status: 'PostStatus!'; + title: 'String!'; + content: 'String!'; + author: 'User!'; + }; + PostStatus: GraphQLEnum<'DRAFT' | 'PUBLISHED' | 'ARCHIVED'>; + + Query: { + users: [NonNullable, '[User!]!']; + post: [{ id: 'Int!' }, 'Post']; + }; +}; + +describe('schema', () => { + it('should validate a GraphQL schema', () => { + const _ = schema({ + User: { + id: 'Int!', + username: 'String!', + email: 'String', + }, + Post: { + id: 'Int!', + status: 'PostStatus!', + title: 'String!', + content: 'String!', + author: 'User!', + }, + PostStatus: enumOf('DRAFT', 'PUBLISHED', 'ARCHIVED'), + + Query: { + users: [{}, '[User!]!'], + post: [{ id: 'Int!' }, 'Post'], + }, + }); + expect(_).to(equal); + $ = _; + }); +}); + +describe('compile', () => { + it('should infer the type of a GraphQL schema', () => { + const $$ = infer($); + + type User = typeof $$.User; + expect().to( + equal<{ + id: number; + username: string; + email: string | null; + }>(), + ); + + type Post = typeof $$.Post; + expect().to( + equal<{ + id: number; + status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED'; + title: string; + content: string; + author: User; + }>(), + ); + }); +}); diff --git a/src/types.ts b/src/types.ts index 6119861..cd1a29e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,11 @@ -import type { StringLiteral, ValueOf } from './types/common'; +import type { StringKeyOf, StringLiteral, ValueOf } from './types/common'; import type { BaseEnvironment, FunctionCollection, GraphQLEnum, TypeCollection, } from './types/graphql-types'; +import type { Parse } from './types/parser'; import type { Validate } from './types/validator'; export const GRAPHQL_BASE_TYPES = ['ID', 'Int', 'Float', 'String', 'Boolean'] as const; @@ -57,6 +58,34 @@ export const schema = < schema: Schema, ) => schema; +/** + * Infer the type of a GraphQL schema in TypeScript. + * @param _ The schema to infer. + * @returns + */ +export const infer = < + $ extends + | { + Query?: FunctionCollection; + Mutation?: FunctionCollection; + Subscription?: FunctionCollection; + } + | TypeCollection, +>( + _: $, +) => { + const createInfiniteProxy = () => + new Proxy({} as T, { + get: (): any => createInfiniteProxy(), + }); + return createInfiniteProxy<{ + [P in StringKeyOf>]: Exclude< + Parse<$[P], $ & BaseEnvironment>, + null + >; + }>(); +}; + /** * Create a GraphQL enum type. * @param values The values of the enum.