简体   繁体   中英

How to use GraphQL with TypeScript and typings generated from graphql-code-generator?

I'm following the Apollo Docs tutorial to build an Apollo Server (Express) with TypeScript and I'm also using GraphQL Code Generator to generate the necessary typings based on my GraphQL schema.

This is my current codegen.json configuration:

{
  "schema": "./lib/schema/index.graphql",
  "generates": {
    "./dist/typings/graphql/schema.d.ts": {
      "plugins": [
        "typescript",
        "typescript-resolvers"
      ],
      "config": {
        "typesPrefix": "GQL",
        "skipTypename": true,
        "noSchemaStitching": true,
        "useIndexSignature": true
      }
    }
  }
}

This is my current GraphQL schema based on the tutorial (it's not complete, I haven't finished the whole thing yet and I've trimmed a few things to make the example smaller):

type Query {
    launch(id: ID!): Launch
}

type Launch {
    id: ID!
    site: String
    mission: Mission
}

enum PatchSize {
    SMALL
    LARGE
}

type Mission {
    name: String
    missionPatch(mission: String, size: PatchSize): String
}

Which generates the following TypeScript typings:

import { GraphQLResolveInfo } from 'graphql';
export type Maybe<T> = T | null;
export type RequireFields<T, K extends keyof T> = { [X in Exclude<keyof T, K>]?: T[X] } & { [P in K]-?: NonNullable<T[P]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string,
  String: string,
  Boolean: boolean,
  Int: number,
  Float: number,
};

export type GQLLaunch = {
  id: Scalars['ID'],
  site?: Maybe<Scalars['String']>,
  mission?: Maybe<GQLMission>,
};

export type GQLMission = {
  name?: Maybe<Scalars['String']>,
  missionPatch?: Maybe<Scalars['String']>,
};


export type GQLMissionMissionPatchArgs = {
  mission?: Maybe<Scalars['String']>,
  size?: Maybe<GQLPatchSize>
};

export enum GQLPatchSize {
  Small = 'SMALL',
  Large = 'LARGE'
}

export type GQLQuery = {
  launch?: Maybe<GQLLaunch>,
};


export type GQLQueryLaunchArgs = {
  id: Scalars['ID']
};

export type WithIndex<TObject> = TObject & Record<string, any>;
export type ResolversObject<TObject> = WithIndex<TObject>;

export type ResolverTypeWrapper<T> = Promise<T> | T;

export type ResolverFn<TResult, TParent, TContext, TArgs> = (
  parent: TParent,
  args: TArgs,
  context: TContext,
  info: GraphQLResolveInfo
) => Promise<TResult> | TResult;

export type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> = ResolverFn<TResult, TParent, TContext, TArgs>;

export type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (
  parent: TParent,
  args: TArgs,
  context: TContext,
  info: GraphQLResolveInfo
) => AsyncIterator<TResult> | Promise<AsyncIterator<TResult>>;

export type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (
  parent: TParent,
  args: TArgs,
  context: TContext,
  info: GraphQLResolveInfo
) => TResult | Promise<TResult>;

export interface SubscriptionSubscriberObject<TResult, TKey extends string, TParent, TContext, TArgs> {
  subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>;
  resolve?: SubscriptionResolveFn<TResult, { [key in TKey]: TResult }, TContext, TArgs>;
}

export interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {
  subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>;
  resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>;
}

export type SubscriptionObject<TResult, TKey extends string, TParent, TContext, TArgs> =
  | SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>
  | SubscriptionResolverObject<TResult, TParent, TContext, TArgs>;

export type SubscriptionResolver<TResult, TKey extends string, TParent = {}, TContext = {}, TArgs = {}> =
  | ((...args: any[]) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)
  | SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>;

export type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (
  parent: TParent,
  context: TContext,
  info: GraphQLResolveInfo
) => Maybe<TTypes>;

export type NextResolverFn<T> = () => Promise<T>;

export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs = {}> = (
  next: NextResolverFn<TResult>,
  parent: TParent,
  args: TArgs,
  context: TContext,
  info: GraphQLResolveInfo
) => TResult | Promise<TResult>;

/** Mapping between all available schema types and the resolvers types */
export type GQLResolversTypes = ResolversObject<{
  Query: ResolverTypeWrapper<{}>,
  ID: ResolverTypeWrapper<Scalars['ID']>,
  Launch: ResolverTypeWrapper<GQLLaunch>,
  String: ResolverTypeWrapper<Scalars['String']>,
  Mission: ResolverTypeWrapper<GQLMission>,
  PatchSize: GQLPatchSize,
  Boolean: ResolverTypeWrapper<Scalars['Boolean']>,
}>;

/** Mapping between all available schema types and the resolvers parents */
export type GQLResolversParentTypes = ResolversObject<{
  Query: {},
  ID: Scalars['ID'],
  Launch: GQLLaunch,
  String: Scalars['String'],
  Mission: GQLMission,
  PatchSize: GQLPatchSize,
  Boolean: Scalars['Boolean'],
}>;

export type GQLLaunchResolvers<ContextType = any, ParentType extends GQLResolversParentTypes['Launch'] = GQLResolversParentTypes['Launch']> = ResolversObject<{
  id?: Resolver<GQLResolversTypes['ID'], ParentType, ContextType>,
  site?: Resolver<Maybe<GQLResolversTypes['String']>, ParentType, ContextType>,
  mission?: Resolver<Maybe<GQLResolversTypes['Mission']>, ParentType, ContextType>,
}>;

export type GQLMissionResolvers<ContextType = any, ParentType extends GQLResolversParentTypes['Mission'] = GQLResolversParentTypes['Mission']> = ResolversObject<{
  name?: Resolver<Maybe<GQLResolversTypes['String']>, ParentType, ContextType>,
  missionPatch?: Resolver<Maybe<GQLResolversTypes['String']>, ParentType, ContextType, GQLMissionMissionPatchArgs>,
}>;

export type GQLQueryResolvers<ContextType = any, ParentType extends GQLResolversParentTypes['Query'] = GQLResolversParentTypes['Query']> = ResolversObject<{
  launch?: Resolver<Maybe<GQLResolversTypes['Launch']>, ParentType, ContextType, RequireFields<GQLQueryLaunchArgs, 'id'>>,
}>;

export type GQLResolvers<ContextType = any> = ResolversObject<{
  Launch?: GQLLaunchResolvers<ContextType>,
  Mission?: GQLMissionResolvers<ContextType>,
  Query?: GQLQueryResolvers<ContextType>,
}>;

This is my resolvers.ts file:

import { GQLPatchSize } from '@typings/graphql/schema';
import { GQLResolvers } from '@typings/graphql/schema';

const resolvers: GQLResolvers = {
    Query: {
        launch: (_, args, { dataSources }) => {
            return dataSources.launchesAPI.getLaunchById(args);
        },
    },
    Mission: {
        missionPatch: (mission, { size } = { size: GQLPatchSize.Large }) => {
            return size === 'SMALL' ? mission.missionPatchSmall : mission.missionPatchLarge;
        },
    },
};

export { resolvers };

And to finish, my launches.ts file with the LaunchesAPI class:

import { GQLLaunch } from '@typings/graphql/schema';
import { GQLQueryLaunchArgs } from '@typings/graphql/schema';
import { RESTDataSource } from 'apollo-datasource-rest';

const SPACEX_API_ENDPOINT = 'https://api.spacexdata.com/v3/';

class LaunchesAPI extends RESTDataSource {
    constructor() {
        super();

        this.baseURL = SPACEX_API_ENDPOINT;
    }

    async getLaunchById({ id }: GQLQueryLaunchArgs) {
        const response = await this.get('launches', { flight_number: id });
        return this.launchReducer(response[0]);
    }

    launchReducer(launch: any): GQLLaunch {
        return {
            id: String(launch.flight_number) || '0',
            site: launch.launch_site && launch.launch_site.site_name,
            mission: {
                name: launch.mission_name,
                missionPatchSmall: launch.links.mission_patch_small,
                missionPatchLarge: launch.links.mission_patch,
            },
        };
    }
}

export { LaunchesAPI };

Now, because I'm typing the result of launchReducer() with GQLLaunch , the mission property type is GQLMission and this type only has two properties, name and missionPatch . It does not have missionPatchSmall or missionPatchLarge and thus I get this error:

Type '{ name: any; missionPatchSmall: any; missionPatchLarge: any; }' is not assignable to type 'GQLMission'. Object literal may only specify known properties, and 'missionPatchSmall' does not exist in type 'GQLMission'. ts(2339)

A similar error exists in the resolvers.ts file when it tries to read mission.missionPatchSmall or mission.missionPatchLarge as they don't exist in the mission object of type GQLMission :

Property 'missionPatchSmall' does not exist on type 'GQLMission'. ts(2339)

I'm not sure how to handle this, suggestions?

You're putting properties on mission that aren't a part of GQLMission and then explicitly typing mission to GQLMission . Put generally, you are attempting to generate your types from your schema, but the return type from your resolver does not match what is specified by your schema.

Most of the time, the challenge you're facing is caused by either some deficiency in schema design or some hackery in resolver implementation.

As such, your options are generally:

  • abandon using schema-generated types for your resolvers ( This is my least favorite option. )
  • change your schema to match your resolver return type ( Resolve schema deficiency. )
  • change your resolver to match your schema return type ( Resolve resolver deficiency. )
  • change both your schema and resolver to return some new shared type ( Resolve schema deficiency and update resolver implementation for new schema. )

Assuming you're intent on moving forward using schema-generated types for your resolvers, we can eliminate option 1 and consider the last three options as applied to your situation.

  1. Yield to your resolver as implementing the correct type, and update your schema to match. This would mean changing the GQLMission type in your schema to match the return type from your resolver (include both missionPatchLarge and missionPatchSmall properties) and allowing your clients to query one or both via their query of the schema directly.
  2. Yield to your schema as the correct type, and update your resolver implementation to match. This would mean getting rid of the excess returned properties ( missionPatchLarge and missionPatchSmall ), which you're currently using to ease implementation, and fetch the appropriate missionPatch value anew in the missionPatchResolver subresolver (ideally hitting cache to prevent perf hit).
  3. Rethink your representation of missionPatch on your schema altogether. Consider the nature of a missionPatch . Is it really an either/or situation? This solution would involve changing the shape of the schema API around size and missionPatch , which would then need to be mirrored on your resolver implementation.

What you do will depend on what the nature of a missionPatch is. My guess is that one of the last three options make sense here. If the two missionPatch types are actually different variants, it may make sense to change missionPatch to missionPatches , which returns an array of MissionPatch objects, which can be filtered by size . If one is derivative of the other, it may make most sense to leave them as separate missionPatch and missionPatchSmall strings exposed through the schema.

Edit: Looking into the api you're using, it's clear these are independent values that could both be requested. There is no such thing as a small or large mission. These are images of different sizes for the same mission. My approach would likely be to include both these values on your schema either directly or on a nested missionPatch property, eg

export type GQLMission = {
  name?: Maybe<Scalars['String']>,

  smallPatchUrl: String,
  largePatchUrl: String,

  # OR

  patch?: MissionPatch,
};

export type MissionPatch = {
  smallUrl: String,
  largeUrl: String
};

Side-note: It isn't uncommon to represent images via their own value object type, which might include urls for different sizes of the image along with details about the image like aspect ratio or native width or height.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM