Skip to content

Latest commit

 

History

History
138 lines (101 loc) · 5.88 KB

types-and-fields.md

File metadata and controls

138 lines (101 loc) · 5.88 KB
title
Types and Fields

The main idea of TypeGraphQL is to automatically create GraphQL schema definitions from TypeScript classes. To avoid the need for schema definition files and interfaces describing the schema, we use decorators and a bit of reflection magic.

Let's start by defining our example TypeScript class which represents our Recipe model with fields for storing the recipe data:

class Recipe {
  id: string;
  title: string;
  ratings: Rate[];
  averageRating?: number;
}

The first thing we must do is decorate the class with the @ObjectType decorator. It marks the class as the type known from the GraphQL SDL or GraphQLObjectType from graphql-js:

@ObjectType()
class Recipe {
  id: string;
  title: string;
  ratings: Rate[];
  averageRating: number;
}

Then we declare which class properties should be mapped to the GraphQL fields. To do this, we use the @Field decorator, which is also used to collect metadata from the TypeScript reflection system:

@ObjectType()
class Recipe {
  @Field()
  id: string;

  @Field()
  title: string;

  @Field()
  ratings: Rate[];

  @Field()
  averageRating: number;
}

For simple types (like string or boolean) this is all that's needed but due to a limitation in TypeScript's reflection, we need to provide info about generic types (like Array or Promise). So to declare the Rate[] type, we have to use the explicit [ ] syntax for array types - @Field(type => [Rate]). For nested arrays, we just use the explicit [ ] notation to determine the depth of the array, e.g. @Field(type => [[Int]]) would tell the compiler we expect an integer array of depth 2.

Why use function syntax and not a simple { type: Rate } config object? Because, by using function syntax we solve the problem of circular dependencies (e.g. Post <--> User), so it was adopted as a convention. You can use the shorthand syntax @Field(() => Rate) if you want to save some keystrokes but it might be less readable for others.

By default, all fields are non nullable, just like properties in TypeScript. However, you can change that behavior by providing nullableByDefault: true option in buildSchema settings, described in bootstrap guide.

So for nullable properties like averageRating which might not be defined when a recipe has no ratings yet, we mark the class property as optional with a ?: operator and also have to pass the { nullable: true } decorator parameter. We should be aware that when we declare our type as a nullable union (e.g. string | null), we need to explicitly provide the type to the @Field decorator.

In the case of lists, we may also need to define their nullability in a more detailed form. The basic { nullable: true | false } setting only applies to the whole list ([Item!] or [Item!]!), so if we need a sparse array, we can control the list items' nullability via nullable: "items" (for [Item]!) or nullable: "itemsAndList" (for the [Item]) option. Be aware that setting nullableByDefault: true option will also apply to lists, so it will produce [Item] type, just like with nullable: "itemsAndList".

For nested lists, those options apply to the whole depth of the array: @Field(() => [[Item]] would by default produce [[Item!]!]!, setting nullable: "itemsAndList" would produce [[Item]] while nullable: "items" would produce [[Item]]!

In the config object we can also provide the description and deprecationReason properties for GraphQL schema purposes.

So after these changes our example class would look like this:

@ObjectType({ description: "The recipe model" })
class Recipe {
  @Field(type => ID)
  id: string;

  @Field({ description: "The title of the recipe" })
  title: string;

  @Field(type => [Rate])
  ratings: Rate[];

  @Field({ nullable: true })
  averageRating?: number;
}

Which will result in generating the following part of the GraphQL schema in SDL:

type Recipe {
  id: ID!
  title: String!
  ratings: [Rate!]!
  averageRating: Float
}

Similarly, the Rate type class would look like this:

@ObjectType()
class Rate {
  @Field(type => Int)
  value: number;

  @Field()
  date: Date;

  user: User;
}

which results in this equivalent of the GraphQL SDL:

type Rate {
  value: Int!
  date: Date!
}

As we can see, for the id property of Recipe we passed type => ID and for the value field of Rate we passed type => Int. This way we can overwrite the inferred type from the reflection metadata. We can read more about the ID and Int scalars in the scalars docs. There is also a section about the built-in Date scalar.

Also the user property doesn't have a @Field() decorator - this way we can hide some properties of our data model. In this case, we need to store the user field of the Rate object to the database in order to prevent multiple rates, but we don't want to make it publicly accessible.

Note that if a field of an object type is purely calculable (e.g. averageRating from ratings array) and we don't want to pollute the class signature, we can omit it and just implement the field resolver (described in resolvers doc).

Be aware that defining constructors is strictly forbidden and we shouldn't use them there, as TypeGraphQL creates instances of object type classes under the hood by itself.

In some case we may want to expose our classes or properties under a different types or fields name. To accomplish this, we can use the name parameter or name property of decorator's options, e.g.:

@ObjectType("ExternalTypeName")
class InternalClassName {
  @Field({ name: "externalFieldName" })
  internalPropertyName: string;
}

However, be aware that renaming fields works only for output types like object type or interface type. It's due to a fact that input fields has no resolvers that could translate one field value into another property value.