Skip to content

Commit

Permalink
🦄 refactor: Improve type information readability
Browse files Browse the repository at this point in the history
  • Loading branch information
Snowflyt committed Mar 9, 2024
1 parent 62ece80 commit ea8eb2d
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 54 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const config = {
rules: {
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
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.0",
"version": "0.1.1",
"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
24 changes: 19 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ type OperationFunction<
: unknown)
: never;

type QueryFunction<
TQueries extends FunctionCollection,
$ extends TypeCollection,
> = OperationFunction<'query', $, TQueries>;
type MutationFunction<
TMutations extends FunctionCollection,
$ extends TypeCollection,
> = OperationFunction<'mutation', $, TMutations>;
type SubscriptionFunction<
TSubscriptions extends FunctionCollection,
$ extends TypeCollection,
> = OperationFunction<'subscription', $, TSubscriptions>;

type AbstractClient = {
getRequestClient: () => RequestClient;
getWSClient: () => WSClient;
Expand All @@ -149,15 +162,16 @@ export type Client<
> = AbstractClient &
(TQueries extends Record<string, never>
? Record<string, never>
: { query: OperationFunction<'query', $, TQueries> }) &
: // HACK: Spread `$` immediately to make type information more readable
{ query: QueryFunction<TQueries, { [P in keyof $]: $[P] }> }) &
(TMutations extends Record<string, never>
? Record<string, never>
: { mutation: OperationFunction<'mutation', $, TMutations> }) &
: // HACK: Spread `$` immediately to make type information more readable
{ mutation: MutationFunction<TMutations, { [P in keyof $]: $[P] }> }) &
(TSubscriptions extends Record<string, never>
? Record<string, never>
: {
subscription: OperationFunction<'subscription', $, TSubscriptions>;
});
: // HACK: Spread `$` immediately to make type information more readable
{ subscription: SubscriptionFunction<TSubscriptions, { [P in keyof $]: $[P] }> });

const _createClient = <
T extends
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,5 @@ export const isGraphQLEnum = (value: unknown): value is GraphQLEnum =>
Array.isArray(value.values) &&
value.values.every((v) => typeof v === 'string');

export const getTypesEnums = <T extends Schema<T>>($: T): string[] =>
export const getTypesEnums = ($: TypeCollection): string[] =>
Object.keys(Object.entries($).filter(isGraphQLEnum));
42 changes: 42 additions & 0 deletions src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ export type CanBeNull<T> = null extends string ? unknown : null extends T ? true
*/
export type CanBeUndefined<T> = null extends string ? unknown : undefined extends T ? true : false;

/**
* Judge whether `T` extends `U`.
*/
export type Extends<T, U> = [T] extends [U] ? true : false;

/*****************
* Utility types *
*****************/
Expand Down Expand Up @@ -194,6 +199,13 @@ export type WithDefault<T, D> = unknown extends T ? D : T;
*/
export type Cast<T, U> = T extends U ? T : never;

/**
* Merge two objects together. Optional keys are not considered.
*/
export type SimpleMerge<L, R> = {
[P in keyof L | keyof R]: P extends keyof R ? R[P] : P extends keyof L ? L[P] : never;
};

/**
* Merge two objects together. Optional keys are considered.
*
Expand All @@ -218,3 +230,33 @@ type _SpreadProperties<L, R, K extends keyof L & keyof R> = {
[P in K]: L[P] | Exclude<R[P], undefined>;
};
type _Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;

// prettier-ignore
export type Letter = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M'
| 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z'
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o'
| 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';

/****************
* Control flow *
****************/
export type Result<T, E> = Ok<T> | Err<E>;
export interface Ok<T> {
readonly __tag: 'Ok';
readonly value: T;
}
export interface Err<E> {
readonly __tag: 'Err';
readonly error: E;
}

/********************
* String utilities *
********************/
export namespace Str {
export type Head<S extends string> = S extends `${infer H}${string}` ? H : never;
export type Tail<S extends string> = S extends `${string}${infer T}` ? T : never;

export type IsEmpty<S extends string> = S extends '' ? true : false;
}
23 changes: 20 additions & 3 deletions src/types/validator.proof.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { describe, error, expect, it } from 'typroof';
import { describe, equal, error, expect, it } from 'typroof';

import { enumOf } from '../types';

import type { BaseEnvironment } from './graphql-types';
import type { BaseEnvironment, GraphQLEnum } from './graphql-types';
import type { Validate } from './validator';

declare const validate: <TAliases>(aliases: Validate<TAliases, BaseEnvironment>) => TAliases;
declare const validate: <T>(aliases: Validate<T, BaseEnvironment>) => Validate<T, BaseEnvironment>;

describe('Validate', () => {
it('should validate object representation of GraphQL type aliases', () => {
type ToValidate = {
User: {
id: 'String!';
username: 'String!';
posts: '[Post!]!';
};
Post: {
id: 'ID!';
title: 'String';
content: 'String!';
author: 'User!';
kind: 'OperatorKindEnum!';
};
OperatorKindEnum: GraphQLEnum<'GTE' | 'LTE' | 'NE' | 'LIKE'>;
};
expect<Validate<ToValidate, BaseEnvironment>>().to(equal<ToValidate>);

expect(
validate({
User: {
Expand Down
155 changes: 111 additions & 44 deletions src/types/validator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import type { Constructor, IsTopType, IsUnknown, StringKeyOf } from './common';
import type {
Constructor,
Digit,
IsTopType,
IsUnknown,
Letter,
SimpleMerge,
Str,
StringKeyOf,
} from './common';
import type { GraphQLEnum, SimpleVariantOf } from './graphql-types';

/*********************
* Message functions *
*********************/
type WriteDuplicateAliasesMessage<TName extends string> = `Type '${TName}' is already defined`;

type WriteUnresolvableMessage<TToken extends string> = `'${TToken}' is unresolvable`;

type WriteUnexpectedCharacterMessage<TChar extends string> = `Unexpected character '${TChar}'`;

type WriteBadDefinitionTypeMessage<TActual extends string> =
`Type definitions must be strings or objects (was ${TActual})`;
type BadDefinitionType = number | bigint | boolean | symbol | null | undefined;
Expand Down Expand Up @@ -80,35 +85,106 @@ type DomainOf<TData> = IsTopType<TData> extends true
*************/
type Evaluate<T> = { [P in keyof T]: T[P] };

type ValidateString<TDef extends string, $> = TDef extends `[${infer TChild}!]!`
? TChild extends StringKeyOf<$>
? TDef
: WriteUnresolvableMessage<TChild>
: TDef extends `[${infer TChild}!]`
? TChild extends StringKeyOf<$>
? TDef
: WriteUnresolvableMessage<TChild>
: TDef extends `[${infer TChild}]!`
? TChild extends StringKeyOf<$>
? TDef
: WriteUnresolvableMessage<TChild>
: TDef extends `[${infer TChild}]`
? TChild extends StringKeyOf<$>
? TDef
: WriteUnresolvableMessage<TChild>
: TDef extends `${infer TChild}!`
? TChild extends StringKeyOf<$>
? TDef
: WriteUnresolvableMessage<TChild>
: TDef extends StringKeyOf<$>
? TDef
: TDef extends `[${string}]!${infer U}${string}`
? WriteUnexpectedCharacterMessage<U>
: TDef extends `[${string}]${infer U}${string}`
? WriteUnexpectedCharacterMessage<U>
: TDef extends `[${string}!${infer U}]${string}`
? WriteUnexpectedCharacterMessage<U>
: SimpleVariantOf<StringKeyOf<$>>;
namespace Scanner {
export interface State {
unscanned: string;
last: string;
terminated: boolean;
error: string | null;
coreType: string;
hasUnclosedLeftBracket: boolean;
}

export type New<TString extends string> = {
unscanned: TString;
last: never;
terminated: false;
error: null;
coreType: '';
hasUnclosedLeftBracket: false;
};

export type Next<TState extends State, $> = Str.Head<
TState['unscanned']
> extends infer C extends string
? [C] extends [never]
? SimpleMerge<
TState,
{
terminated: true;
error: TState['hasUnclosedLeftBracket'] extends true
? TState['last'] extends '!'
? "Missing expected ']'"
: "Missing expected ']' or '!'"
: TState['coreType'] extends StringKeyOf<$>
? null
: `'${TState['coreType']}' is unresolvable`;
}
>
: SimpleMerge<
TState,
SimpleMerge<
{
unscanned: Str.Tail<TState['unscanned']>;
last: C;
},
[TState['last']] extends [never]
? C extends '['
? { hasUnclosedLeftBracket: true }
: C extends '_' | Letter | Digit
? { coreType: C }
: { error: `Unexpected character '${C}'` }
: TState['last'] extends ']'
? C extends '!'
? NonNullable<unknown>
: { error: `Unexpected character '${C}'` }
: TState['last'] extends '!'
? [TState['hasUnclosedLeftBracket'], C] extends [true, ']']
? { hasUnclosedLeftBracket: false }
: C extends ']'
? { error: "Missing expected '[' at the beginning" }
: { error: `Unexpected character '${C}'` }
: C extends ']'
? TState['hasUnclosedLeftBracket'] extends true
? {
hasUnclosedLeftBracket: false;
error: TState['coreType'] extends StringKeyOf<$>
? null
: `'${TState['coreType']}' is unresolvable`;
}
: { error: "Missing expected '[' at the beginning" }
: C extends '!'
? TState['coreType'] extends ''
? { error: "Missing expected type before '!'" }
: {
error: TState['coreType'] extends StringKeyOf<$>
? null
: `'${TState['coreType']}' is unresolvable`;
}
: C extends Letter | Digit
? { coreType: `${TState['coreType']}${C}` }
: { error: `Unexpected character '${C}'` }
>
>
: never;
}

type _ValidateString<TDef extends string, TState extends Scanner.State, $> = Scanner.Next<
TState,
$
> extends infer TNext
? TNext extends Scanner.State
? TNext['error'] extends null
? TNext['terminated'] extends true
? TDef
: _ValidateString<TDef, TNext, $>
: TNext['error']
: never
: never;

type ValidateString<TDef extends string, $> = TDef extends ''
? SimpleVariantOf<StringKeyOf<$>> // Provide better intellisense for empty string
: _ValidateString<TDef, Scanner.New<TDef>, $>;

type ValidateDefinition<TDef, $> = TDef extends string
? ValidateString<TDef, $>
Expand All @@ -124,20 +200,11 @@ type ValidateDefinition<TDef, $> = TDef extends string
? [ValidateDefinition<TVariables, $>, 'void']
: Evaluate<{ [P in keyof TDef]: ValidateDefinition<TDef[P], $> }>;

declare const id: unique symbol;
type Nominal<T, Id extends string> = T & { readonly [id]: Id };

type Alias<TDef = NonNullable<unknown>> = Nominal<TDef, 'alias'>;

type BootstrapScope<TAliases, TEnvironment> = {
[P in keyof TAliases]: Alias<TAliases[P]>;
} & TEnvironment;

/**
* Validate GraphQL type aliases.
*/
export type Validate<TAliases, TEnvironment> = Evaluate<{
[P in keyof TAliases]: P extends StringKeyOf<TEnvironment>
? WriteDuplicateAliasesMessage<P & string>
: ValidateDefinition<TAliases[P], BootstrapScope<TAliases, TEnvironment>>;
: ValidateDefinition<TAliases[P], TAliases & TEnvironment>;
}>;

0 comments on commit ea8eb2d

Please sign in to comment.