A TypeScript library for building GraphQL APIs efficiently with type safe decora...
source link: https://www.tuicool.com/articles/hit/7ZZV3e7
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
ts-graphql
A TypeScript library for building GraphQL APIs efficiently with type safe decorators.
Project goals:
- As close to 100% type safety between GraphQL schema and code as possible; if it compiles, there shouldn't be runtime type errors
- Single source of truth for both schema types and TS types
- Lightweight wrapper around graphql-js - stay flexible and easy to comprehend
Table of Contents
Installation
yarn add ts-graphql # or npm i ts-graphql
Usage
To quickly try out the library, you can clone it and run the examples . E.g. npx ts-node examples/interface/index.ts
Patterns
The goal is to keep this simple and unopinionated. Most decorators/functions for creating types accept all the same options as their counterparts in graphql-js
, but with a differently typed type
option. For specifics, see the type definitions.
The only special patterns needed are:
- 1:1 mapping between GraphQL types and TS types
- For it to be typed, Context has to be a class (see)
Standard Scalars
TS GraphQL provides wrapped versions of all the built-in scalars:
import { TSGraphQLBoolean, TSGraphQLFloat, TSGraphQLID, TSGraphQLInt, TSGraphQLString, } from 'ts-graphql';
Object Types
Object types use the decorators ObjectType
and Field
. You can also define fields separately from the source class, see.
For Field
, InputField
, and Arg
, you can leave out the type
option for properties explicitly typed as string
, number
, or boolean
. For methods and other types it is required (this is enforced with TS types). It is best to explicitly set the type though - see
The Field
decorator can be applied to a property or a method. The method takes parameters args
, context
and info
. source
is left out as it is available as this
.
import { ObjectType, Field, TSGraphQLInt, TSGraphQLString, } from 'ts-graphql'; @ObjectType() class Vehicle { @Field({ description: 'Make of the vehicle' }) make: string; @Field({ description: 'Model of the vehicle' }) model: string; @Field({ type: TSGraphQLInt, description: 'Year the vehicle was produced' }) year: number; @Field({ type: TSGraphQLString }) title() { return `${this.year} ${this.make} ${this.model}`; } //... }
Input Object Types
Input objects have different decorators from output objects: InputObjectType
and InputField
.
import { InputObjectType, InputField, TSGraphQLID, } from 'ts-graphql'; @InputObjectType() class ServiceRequestInput { @InputField({ type: TSGraphQLID }) vehicleID!: string | number; // You can use property initializers to specify the default value @InputField() description: string = ''; // Or the config option defaultValue @InputField({ type: TSGraphQLInt, defaultValue: 55, }) code!: number; }
Args
Args have the same config options and behavior as input object types. Arg and input object type classes will be instantiated, so if you want to you can add methods, getters/setters, etc.
import { Args, Arg, ObjectType, Field, } from 'ts-graphql'; @Args() class ServiceRequestArgs { @Arg({ type: ServiceRequestInput }) input!: ServiceRequestInput; } // ... // Usage @ObjectType() class Mutation { @Field({ type: ServiceRequestPayload, args: ServiceRequestArgs }) requestService(args, context) { // args instanceof ServiceRequestArgs === true const { input } = args; // input instanceof ServiceRequestInput === true const { vehicleID } = input; // ... } }
Enums
You can use TS enums in your code, and create a type for TS GraphQL to use.
import { enumType, EnumTypeCase, Field } from 'ts-graphql'; enum Shape { Square, Circle, Triangle, } // In schema will be: Square, Circle, Triangle const ShapeType = enumType(Shape); // Or if you want constant case in schema (SQUARE, CIRCLE, TRIANGLE) const ShapeType = enumType(Shape, { changeCase: EnumTypeCase.Constant, }); // In an object type... @Field({ type: ShapeType }) shape() { return Shape.Circle; }
You can set description
and deprecationReason
for enum values with additional
:
const ShapeType = enumType(Shape, { additional: { Square: { description: '4 sides, all of equal length' }, }, });
Union Types
Union types are a little verbose, but there isn't really a way around it:
import { ObjectType, Field, unionType, } from 'ts-graphql'; @ObjectType() class A { @Field() a!: string; } @ObjectType() class B { @Field() b!: string; } const AOrB = unionType<A | B>({ name: 'AOrB', types: [A, B], });
Root Types
There aren't any special functions for the root types, they are just object types.
However, if you create them as classes, type safety is a little off as you won't have access to instances of those classes - resolver methods will be bound to whatever the root value is.
The best thing to do is usewith the source set to the type of your root value (if undefined
, can leave it out) and then use buildFields
to create a GraphQLObjectType
:
import { GraphQLObjectType } from 'graphql'; import { buildFields } from 'ts-graphql'; import fooQueryFields from './foo'; import barQueryFields from './bar'; // ... const Query = new GraphQLObjectType({ name: 'Query', fields: () => buildFields([ fooQueryFields, barQueryFields, ]), });
Schema
ts-graphql
doesn't currently provide its own way of building a schema. What it provides are functions for generating types that the GraphQLSchema
constructor can accept:
import { buildObjectType, buildNamedTypes } from 'ts-graphql'; import { GraphQLSchema } from 'graphql' // ... const schema = new GraphQLSchema({ query: buildObjectType(Query), // or if you followed whats above, just `Query` mutation: buildObjectType(Mutation), types: buildNamedTypes([ Foo, Bar, ]), });
Context
For context to be type checked, it must be an instance of a class.
class Context { constructor (public viewerId: string) {} }
For resolver methods, you can pass the context option:
@ObjectType() class Foo { @Field({ type: TSGraphQLString , context: Context, }) bar(args: {}, context: Context) { return 'foobar'; } }
However, you'll most likely want your context type to be the same in every resolver. You can create a field decorator bound to your context type and use that instead of Field
from ts-graphql
:
// Field.ts import Context from './Context'; import { fieldDecoratorForContext } from 'ts-graphql'; export default fieldDecoratorForContext(Context); // Elsewhere import Field from './Field'; // And use normally
For modular fields, pass the context
option to fields
and it will be typed in your resolvers. Note that all fields you pass to an ObjectType
must have the same Context
type.
Modular Fields
You can use the fields
method to define fields separately from your object type source, and split them up if you want. This works well for the root types.
// Foo.ts import { ObjectType } from 'ts-graphql'; import { fooFieldsA } from './features/a.ts' import { fooFieldsB } from './features/b.ts' @ObjectType({ fields: () => [fooFieldsA, fooFieldsB], }) export default class Foo { data: string; // ... } // features/a.ts import { fields, TSGraphQLString } from 'ts-graphql'; import Foo from '../Foo.ts'; export const fooFieldsA = fields({ source: Foo }, (field) => ({ data: field( { type: TSGraphQLString }, (source) => source.data, ), })); // features/b.ts import { fields, TSGraphQLInt } from 'ts-graphql'; import Foo from '../Foo.ts'; export const fooFieldsB = fields({ source: Foo }, (field) => ({ dataLength: field( { type: TSGraphQLInt }, (source) => source.data.length, ), }));
Custom Scalars
For your own scalars you can use scalarType
:
import { scalarType } from 'ts-graphql'; // ... const Date = scalarType({ name: 'Date', description: 'ISO-8601 string', serialize, parseValue, parseLiteral, });
Or, you can wrap custom scalars, providing the TS type to associate:
import { wrapScalar } from 'ts-graphql'; import SomeScalar from 'some-scalar'; const SomeScalarTyped = wrapScalar<SomeType>(SomeScalar);
Why?
This library is the result of experiencing many frustrations while working with GraphQL and TypeScript, whether that was programmatically with graphql-js, or writing schemas in the SDL and using something like graphql-cli to generate types from them.
The main issues:
-
Either way, you have to write types twice. Even worse, since the SDL is either strings or text files, you can't use features of the language to DRY up common args and types.
-
There is a disconnect between return values of resolvers and the field types of the schema. For example, the resolver can return null, but the schema has it marked as non null. This is a runtime error, and one that can't be caught immediately, so unless you have every single field tested with every possible condition, the error won't be thrown until some point later in the QA cycle, potentially even after deployment.
This library solves both of those:
extends
Caveats
There are a few things that can't be caught at compile time:
Nullable Input Fields
Nullable fields on input types don't enforce that the TS property is nullable. This is because T
is assignable to T | null
, which works fine for output types but not so much for input. There might be a way to type this correctly but haven't figured it out yet.
Input/Output type checking
Input types used where an output type is expected and vice versa won't show an error at compile time, they will immediately throw a runtime error though.
Implicit Types
Leaving out the field type for properties implicitly typed as primitives won't throw a compile time error, but will immediately throw at runtime. For example:
@Field() shape: 'circle' | 'square'; @Field() color = 'red';
Matching object types
Because TS is "duck-typed", if you manage to have two classes used for object types that have the exact same fields, returning the wrong class can't be caught at build time. E.g:
@ObjectType() class A { @Field() foo!: string; } @ObjectType() class B { @Field() foo!: string } @ObjectType() class C { @Field({ type: A }) a() { return new B(); } }
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK