Skip to content

Commit

Permalink
✨ feat: Add infer to infer TypeScript type from GraphQL schema
Browse files Browse the repository at this point in the history
  • Loading branch information
Snowflyt committed Mar 9, 2024
1 parent 2cd9ccb commit 20e45b8
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 4 deletions.
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const allUsers = await query('users').select((user) => [
]),
]);

/**
/*
* The type of `post` is inferred as
* {
* title: string;
Expand Down Expand Up @@ -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.<Type>` 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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
79 changes: 79 additions & 0 deletions src/types.proof.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>, '[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<typeof $>);
$ = _;
});
});

describe('compile', () => {
it('should infer the type of a GraphQL schema', () => {
const $$ = infer($);

type User = typeof $$.User;
expect<User>().to(
equal<{
id: number;
username: string;
email: string | null;
}>(),
);

type Post = typeof $$.Post;
expect<Post>().to(
equal<{
id: number;
status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
title: string;
content: string;
author: User;
}>(),
);
});
});
31 changes: 30 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -57,6 +58,34 @@ export const schema = <
schema: Schema<T>,
) => 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 = <T extends object>() =>
new Proxy({} as T, {
get: (): any => createInfiniteProxy(),
});
return createInfiniteProxy<{
[P in StringKeyOf<Omit<$, 'Query' | 'Mutation' | 'Subscription'>>]: Exclude<
Parse<$[P], $ & BaseEnvironment>,
null
>;
}>();
};

/**
* Create a GraphQL enum type.
* @param values The values of the enum.
Expand Down

0 comments on commit 20e45b8

Please sign in to comment.