Skip to content

Repository files navigation

gql-typed - Strongly typed GraphQL

WORK IN PROGRESS: Not tested heavily yet, Type-level validation is not yet implemented.

Motivation

When building GraphQL APIs in TypeScript, it’s easy to end up with runtime-only type safety—your TypeScript code might compile just fine, but at runtime your schema can still contain:

  • Fields that don’t match the actual TypeScript types.
  • Missing or mismatched resolvers.
  • Incorrect arguments, nullability, or return types.
  • Schema definitions that drift from your resolvers over time.

Existing GraphQL libraries (like type-graphql, nexus) improve the developer experience, but they usually rely on runtime validation or code generation to get some type safety. This often means:

  • You only notice mismatches when you run the app or tests.
  • You may need an extra build step for generating types from SDL.
  • You can accidentally deploy an invalid schema without knowing.

gql-typed solves this by pushing schema validation into the TypeScript type system itself:

  • Type errors at compile time if your schema is invalid.
  • No separate code generation step.
  • The schema itself is a typed object, ensuring resolvers match field definitions.
  • Works incrementally across multiple files, without having to declare everything in one place.
  • Produces plain graphql types at runtime, so you can integrate with any existing GraphQL server.

In short: If your TypeScript compiles, your GraphQL schema is valid.

Usage Example

// Config.tsexportinterfaceConfig{// global config for your schemacontext: Context;// ... and with optional configurations, like scalarMap, etc.}// User.tsexportinterfaceUser{id: string;name: string;email: string;}// GraphQLUser.tsexportconstGraphQLUser=objectType<Config,User>()({name: "User",fields: {id: {type: {type: "ID"}},name: {description: "login name",// optional descriptiontype: {type: "String"},},},fieldResolvers: {email: resolver<string|undefined>()()({// say email is authorized fieldtype: {nullable: true,type: "String"},resolve(user,_args,context,_info){// this will be passed to graphqlreturncontext.diContainer.resolve(UserService).getUserEmail(user);},}),
postsConnection,// field resolvers can be defined across multiple files},});// fieldResolvers/Query/userById.tsexportconstuserById=resolver<User|null,Record<"id",string>>()<Config,// in across multiple files, additional generic arguments are required{}// type of the parent(`Query`) object (`{}` in this example)>()({type: {nullable: true,type: "User"},args: {id: {description: "user id",type: {type: "ID"}},},resolve(_parent,args,context,_info){returncontext.diContainer.resolve(UserService).getUserById(args.id);},deprecationReason: "use user instead",});// GraphQLQuery.tsexportconstGraphQLQuery=objectType<Config,undefined>()({name: "Query",fieldResolvers: { userById, ...andOtherFieldsInQuery},});// schema.tsexportconstschema=create<Config>().register(GraphQLQuery,GraphQLUser, ...otherTypes).build({Query: {},// as `{}` is used to represent `Query` object typeMutation: {},}).schema;// you can chain .register(), or print the schema to a file as well// type of `schema` will be error message if your schema has validation errors(<T>(_: Exclude<T,string>)=>{})<typeofschema>(schema);// type error if so

Why fn()()(input)?

In TypeScript, you can’t partially infer and partially provide generics in a single call in a clean way. The currying-like syntax:

objectType<Config,User>()({ ... })

allows you to:

  • Pass some generics explicitly.
  • Let the rest be inferred from the input object.
  • Avoid declaring the object separately with as const just to get proper inference.

Without this, you’d need to do:

constdef={ ... }asconst;objectType<Config,User,typeofdef>(def);

which is verbose and less ergonomic.

If you’re a pure JavaScript user, you can just use the graphql package directly—gql-typed is essentially a type-safe wrapper with compile-time validation.

Features

  • Full compile-time schema validation — no runtime mismatch surprises.
  • Type-safe resolvers — resolver args, parent, and return type are all checked.
  • Multi-file schema definition — works even if field resolvers are split across files.
  • Integrates with existing GraphQL servers — returns standard graphql package objects.
  • No code generation — no extra build step required.
  • Clear, early feedback — if your schema is invalid, TypeScript tells you before running the app.

About

Strongly typed GraphQL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages