2

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?

1 Answer 1

5

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, e.g.

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.

Sign up to request clarification or add additional context in comments.

5 Comments

I was trying to simply follow the tutorial and convert it to TypeScript but that little detail doesn't look like the schema makes much sense. About your options... 1) Doesn't work because then I'd have to change the type on the resolvers too and it would be a mess; 2) I've reached this conclusion before, works, but takes a different direction from the tutorial, but maybe makes more sense? 3) Didn't understand this one, can you provide an example? 4) Also don't know what you mean here, feels like it's similar to the second point?
Also, both mission patches, from the REST API are both strings, but a full URL pointing to an image. One is a large image, the other is a small version.
If you could edit your answer with examples on each of those options that would be awesome. It would be a great answer to the question and most importantly, it would help me visualize exactly what you are suggesting. I'm very new to GraphQL and my understanding of what you're trying saying may be wrong. The examples would really help with that, if you could spare the time to write them. Thanks.
I added some more detail in there. Hopefully that cleared things up. My honest opinion is that you made a decision in your schema design, which didn't accurately represent the entity you're providing, which left you doing some hackery in your resolver implementation to accommodate. With GraphQL, more often than not, if you're finding something to be difficult or complex when you feel it shouldn't be, there's a good chance you've misunderstood the entity you're trying to represent in your schema.
This is a great answer!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.