Hone logo
Hone
Problems

Type-Safe GraphQL Schema Builder

This challenge asks you to build a foundational type-safe GraphQL schema builder in TypeScript. The goal is to create a system that allows developers to define GraphQL schemas using TypeScript types, ensuring that the resulting schema is validated against those types at compile time, significantly reducing runtime errors and improving developer experience.

Problem Description

You need to implement a TypeScript-based builder that generates a GraphQL schema definition. This builder should leverage TypeScript's type system to enforce the structure and types of your schema.

Key Requirements:

  1. Type Definition: The builder must accept type definitions for your GraphQL types (e.g., Query, Mutation, Subscription, and object types) using TypeScript classes or interfaces.
  2. Field Definition: It should provide a mechanism to define fields within each GraphQL type, including their arguments and return types. These should also be strongly typed.
  3. Schema Generation: The builder must output a standard GraphQL schema definition string (SDL - Schema Definition Language) that can be used with GraphQL servers like graphql-js.
  4. Type Safety: At compile time, the system must ensure that:
    • Return types of fields match their declared types.
    • Argument types of fields are correctly defined and used.
    • References between types (e.g., an object type returning another object type) are valid.
  5. Extensibility: The builder should be designed with extensibility in mind, allowing for future additions like custom scalars, directives, and interfaces.

Expected Behavior:

When you use the builder to define a schema, and then generate the SDL, the SDL should accurately reflect the defined types and fields. If there's a type mismatch (e.g., a field is declared to return a String but its implementation conceptually returns a Number), the TypeScript compiler should flag this as an error.

Edge Cases to Consider:

  • Nullable vs. Non-Nullable fields and arguments.
  • List types (e.g., [String], [User!]!).
  • Defining a field that returns a union or an interface.
  • Circular dependencies between object types (though ideally, the builder should help avoid these through clear typing).

Examples

Example 1: Simple Query Type

// Input (conceptual, how you'd use the builder)

interface User {
  id: string;
  name: string;
}

const builder = new SchemaBuilder();

builder.addQueryType({
  name: 'Query',
  fields: {
    hello: {
      type: 'String', // Return type
      resolve: () => 'Hello, world!'
    },
    getUser: {
      type: 'User', // Return type is another object type
      args: {
        id: { type: 'ID!' } // Non-nullable ID argument
      },
      resolve: (parent, args: { id: string }) => ({ id: args.id, name: 'John Doe' })
    }
  }
});

builder.addObjectTypes({
  User: {
    fields: {
      id: { type: 'ID!' },
      name: { type: 'String!' }
    }
  }
});

// builder.buildSchema(); // This would generate the SDL

// Expected Output (GraphQL SDL)

// type Query {
//   hello: String
//   getUser(id: ID!): User
// }

// type User {
//   id: ID!
//   name: String!
// }

Explanation: This example shows defining a Query type with two fields: hello returning a String, and getUser taking a non-nullable ID and returning a User type. It also defines the User object type.

Example 2: Mutation and Input Types

// Input (conceptual)

interface CreateUserInput {
  name: string;
  email: string;
}

interface User {
  id: string;
  name: string;
  email: string;
}

const builder = new SchemaBuilder();

builder.addMutationType({
  name: 'Mutation',
  fields: {
    createUser: {
      type: 'User', // Return type
      args: {
        input: { type: 'CreateUserInput!' } // Non-nullable input type argument
      },
      resolve: (parent, args: { input: CreateUserInput }) => ({
        id: '123',
        name: args.input.name,
        email: args.input.email
      })
    }
  }
});

builder.addInputTypes({
  CreateUserInput: {
    fields: {
      name: { type: 'String!' },
      email: { type: 'String!' }
    }
  }
});

builder.addObjectTypes({
  User: {
    fields: {
      id: { type: 'ID!' },
      name: { type: 'String!' },
      email: { type: 'String!' }
    }
  }
});

// builder.buildSchema();

// Expected Output (GraphQL SDL)

// type Mutation {
//   createUser(input: CreateUserInput!): User
// }

// input CreateUserInput {
//   name: String!
//   email: String!
// }

// type User {
//   id: ID!
//   name: String!
//   email: String!
// }

Explanation: This demonstrates defining a Mutation type, including an input type (CreateUserInput) and a field that returns an object type (User).

Example 3: Type Safety Enforcement (Conceptual Error)

// Input (conceptual - demonstrating a type error)

const builder = new SchemaBuilder();

// Suppose the builder has a mechanism to infer return types from resolver functions
// or requires explicit declaration, and it should check for mismatches.

builder.addQueryType({
  name: 'Query',
  fields: {
    getGreeting: {
      // Problem: Declaring return type as 'Int' but resolver returns a string.
      // A robust builder would catch this at compile time.
      type: 'Int',
      resolve: () => 'Hello, world!' // This should be a type error
    }
  }
});

// A correct version would look like:
// builder.addQueryType({
//   name: 'Query',
//   fields: {
//     getGreeting: {
//       type: 'String',
//       resolve: () => 'Hello, world!'
//     }
//   }
// });

Explanation: If the builder tries to validate the declared type against the actual return type of the resolve function (or if it's expecting a specific type and the resolver provides something else), a type error should occur during compilation.

Constraints

  • The builder must be implemented entirely in TypeScript.
  • The output must be a valid GraphQL Schema Definition Language (SDL) string.
  • The builder should aim for clarity and readability in its API.
  • Performance is not a primary concern for this foundational challenge, but the generated SDL should be efficient.

Notes

  • Consider how you will represent GraphQL types (Object, Scalar, Input, Enum, Union, Interface) within your TypeScript builder.
  • Think about how to handle nullability (! suffix in SDL).
  • The core challenge is bridging the gap between TypeScript's static types and GraphQL's dynamic schema definition, ensuring compile-time safety.
  • You might want to use a library like graphql for actual schema validation or parsing, but the primary goal is to build the builder itself. You don't necessarily need to run the GraphQL server, just generate the schema definition string correctly.
  • Pay attention to how you define arguments for fields.
Loading editor...
typescript