DocumentationMigration Guidestypescript-operations and client-preset v5.0 -> v6.0

Migrating to typecscript-operations and client-preset 6.0

💡

This major version has not been released. You can find the upcoming changes and Alpha version releases link in the feature branch.

What’s new?

typescript-operations and client-preset v6.0 come with a major overhaul of type generation and config for better DX.

  1. Type generation and usage changes
  2. Configuration and dependency changes
  3. Other bug fixes and quality of life improvements

For the most important changes, read the Breaking changes section.

For a full list of changes, see the CHANGELOG.

Installation

Install the new versions of official plugins that are in your dependencies:

npm i -D @graphql-codegen/cli@latest @graphql-codegen/typescript-operations@latest @graphql-codegen/client-preset@latest
⚠️

GraphQL Codegen packages share a lot of code internally, so if you have installed other official packages (such as @graphql-codegen/visitor-plugin-common or @graphql-codegen/typescript-resolvers) explicitly in the same repo, be sure to update them at the same time to avoid unexpected issues.

Migration

client-preset

client-preset already applies the recommended setup, you won’t have to make any changes to default config:

codegen.ts
const config: CodegenConfig = {
  // ...
  generates: {
    'src/gql/': {
      preset: 'client'
    }
  }
}

typescript-operations

typescript-operations can be used in a variety of custom setup, this section aims to explain the changes in the most popular setup.

One-file setup

Previously, this setup required the typescript plugin, and generated all schema and Operation types into one file.

Now, you can remove typescript plugin as typescript-operations works by itself. It also only generates used Input, Enum and operations types.

codegen.ts
const config: CodegenConfig = {
  // ...
  generates: {
    'src/graphql/types.generated.ts': {
      plugins: ['typescript', 'typescript-operations'],
      plugins: ['typescript-operations']
    }
  }
}

Multi-file setup

Some repos may have multiple Codegen projects, each generating types for operations within its scope. In such cases, users may want to re-use the base Input and Enum types generated by typescript plugin with import-types preset:

codegen.ts
const config: CodegenConfig = {
  // ...
  generates: {
    'src/shared/base-types.generated.ts': {
      plugins: ['typescript']
    },
    'src/project-1/types.generated.ts': {
      documents: 'src/project-1/**/*.graphql.ts',
      preset: 'import-types',
      plugins: ['typescript-operations'],
      presetConfig: {
        typesPath: '../shared/base-types.generated.ts'
      }
    },
    'src/project-2/types.generated.ts': {
      documents: 'src/project-2/**/*.graphql.ts',
      preset: 'import-types',
      plugins: ['typescript-operations'],
      presetConfig: {
        typesPath: '../shared/base-types.generated.ts'
      }
    }
  }
}

Now, it is possible to do this with just typescript-operations, as it supports this approach using its own generateOperationTypes and importSchemaTypesFrom options:

codegen.ts
const config: CodegenConfig = {
  // ...
  generates: {
    'src/shared/base-types.generated.ts': {
      documents: 'src/**/*.graphql.ts' // Parses all files with GraphQL documents to generate Enum and Input types that are used by every project
      plugins: ['typescript-operations'],
      config: {
        generateOperationTypes: false, // `generateOperationTypes:false` means only Input, Enum and shared utility types are generated
      }
    },
    'src/project-1/types.generated.ts': {
      documents: 'src/project-1/**/*.graphql.ts', // Only parses GraphQL documents within project-1's scope
      plugins: ['typescript-operations'],
      config: {
        importSchemaTypesFrom: 'src/shared/base-types.generated.ts', // this path is relative to Codegen config location (unlike `typesPath` in the old setup)
      }
    },
    'src/project-2/types.generated.ts': {
      documents: 'src/project-2/**/*.graphql.ts', // Only parses GraphQL documents within project-2's scope
      plugins: ['typescript-operations'],
      config: {
        importSchemaTypesFrom: 'src/shared/base-types.generated.ts',
      },
    }
  }
}

Breaking changes

  1. Object types are no longer generated

Previously, Object types from the schema were generated via the typescript plugin, for example:

// Example of a schema User Object type being previously generated
export type User = {
  __typename?: 'User'
  id: Scalars['ID']['output']
  name: Scalars['String']['output']
}

These types contain all the fields from the schema. However, it’s expected in GraphQL operations to not fetch all fields in practice, so these types should never be used. In reality, these types are often used (intentionally or accidentally) in application code because they are generated.

Now, Object types are no longer generated. Operation types (Variables and Result) are generated based on fields in the documents so these should always be used for client types.

If you need schema types for any reasons, please generate them using typescript plugin into a separate file.

  1. Args types are no longer generated

Args types are only used for server use cases, so they are no longer generated for client use cases.

  1. Scalar types are no longer generated as re-useable type

Previously, Scalar types from the schema were generated into an object and re-used in Variables types:

// All native and custom scalars found in the schema were previously generated
export type Scalars = {
  ID: { input: string | number; output: string }
  String: { input: string; output: string }
  Boolean: { input: boolean; output: boolean }
  Int: { input: number; output: number }
  Float: { input: number; output: number }
}

Now, scalars in Input and Variables types are consistently inlined (similar to Result types) to avoid Scalar utility type usage:

types.generated.ts
export type Scalars = {
  ID: { input: string | number; output: string }
  String: { input: string; output: string }
  Boolean: { input: boolean; output: boolean }
  Int: { input: number; output: number }
  Float: { input: number; output: number }
}
 
export type UserInput = {
  id: Scalars['ID']['input']
  id: string | number
}
 
export type UserVariables = Exact<{
  id: Scalars['ID']['input']
  id: string | number
}>
  1. Input and Enum types are only generated when used

Previously, all Input and Enum types were generated even if they were not used. This could increase bundle size when Enums that incur runtime are used e.g. when native TypeScript enum or const enum are used.

Now, only Input and Enum types used in operations are generated.

  1. __typename is only generated when used

Previously, __typename fields in Result types are generated as optional by default, even when they were not requested:

query User {
  user {
    # Note: __typename is not in the selection set
    id
  }
}

Previously, the above operation resulted in a type with optional __typename:

export type UserQuery = {
  user: {
    __typename?: 'User'
    id: string
  }
}

Now, __typename is not generated by default when it is not in the selection set:

export type UserQuery = {
  user: {
    __typename?: 'User'
    id: string
  }
}
💡

Some clients, such as Apollo Client, automatically request __typename. To achieve the same behaviour you can continue to use skipTypeNameForRoot and nonOptionalTypename options to configure expected type behaviours.

  1. Document field types are generated to correctly match runtime expectation

Previously, nullable fields in Result types were generated as optional by default:

export type UserQuery = {
  user: {
    age?: string | null
  }
}

Now, nullable fields in Result types are never optional (except in some cases e.g. when @defer, @skip or @include are used)

export type UserQuery = {
  user: {
    age?: string | null
    age: string | null
  }
}
  1. Enum config options are consolidated and default value is changed

Previously, there were 4 boolean options to set which Enum variant to generate. However, when used together, the options override one another. This behaviour was unexpected and confusing for both users and maintainers.

Now, enumType is the only config option to use. The default has also been changed to string-literal because that is the option which does not incur runtime cost.

Enum typeExamplesPrevious configNew config
String literaltype UserRole = 'Admin' | 'Customer'{enumsAsTypes:true}{} or {enumType:'string-literal'}
Constexport const UserRole = { Admin: 'ADMIN', Customer: 'CUSTOMER' } as const;{enumsAsConst:true}{enumType:'const'}
Nativeexport enum UserRole { Admin = 'ADMIN', Customer = 'CUSTOMER' };{} or {constEnums:false}{enumType:'native'}
Native constexport const enum UserRole { Admin = 'ADMIN', Customer = 'CUSTOMER' };{constEnums:true}{enumType:'native-const'}
Native numericexport enum UserRole { Admin = 0, Customer = 1 }{numericEnums:true}{enumType:'native-numeric'}
  1. avoidOptionals option is updated to only handle Operation types

Previously, avoidOptionals was shared with the typescript plugin. As the result, some inner options did not affect Operation types (such as avoidOptionals.resolvers, avoidOptionals.query, avoidOptionals.mutation, avoidOptionals.subscription).

Now, there are only 3 inner options, and when turned on, each forces the respective use case to pass in non-optional value.

  • avoidOptionals.variableValue
  • avoidOptionals.inputValue
  • avoidOptionals.defaultValue

Note that the default is false, and you can still use avoidOptionals:true to turn on all options, without having to set each one individually.

  1. preResolveTypes option is removed

preResolveTypes option was used to generate Result types inline (preResolveTypes:false) or use the ones generated by the typescript plugin (preResolveTypes:true). This was bad for a few reasons:

  1. it added dependency to the typescript plugin
  2. true and false had no functional difference to the users
  3. keeping this option doubled the maintenance overhead with no real benefits

preResolveTypes:true has been the default (and very stable) for a long time. It should be used by majority of users by now. So, removing this option makes sense for us.

If you are seeing problems, please create an issue here.

  1. Legacy utility types are removed

The following utility types have been removed:

  • Maybe: used to handle nullability types of fields in Result types. However, field types have been pre-resolved and inlined for a long time, so this option is no longer needed.
  • InputMaybe: used to handle nullability types of Input. Input types are now inlined, so this option is longer needed.
  • MakeOptional, MakeMaybe and MakeEmpty: used to handle preResolveTypes:false. However, preResolveTypes has been removed, so these options are no longer needed.
  1. Make unknown the default type instead of any for custom scalars

Previously, custom Scalars default type was any which bypassed typecheck.

Now, the default type is unknown to ensure data is handled carefully by users.

  1. Make string | number the default type for the native ID scalar

Previously, one set of default Scalar type was shared between client and server plugins via the typescript plugin dependency. This meant it was not possible to set the default for client, as it would complicate the server config, and vice versa. See this PR for more details.

Now, typescript plugin is no longer a dependency. So, we can set the default type as string | number, which is the correct type for the client use case. For more details on how Scalar coercion works here, please read The Complete GraphQL Scalar Guide.