Migrating to typescript-operations and client-preset v6.0
Migrating `typescript-operations` and `client-preset` from v5 to v6. What has changed? How to migrate? What are the new features?
What’s new?
typescript-operations and client-preset v6.0 come with a major overhaul of type generation and
config to improve developer experience.
- Type generation and usage changes
- Configuration and dependency changes
- 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 release notes.
Installation
Install the latest versions of the official plugins in your dependencies:
Migration
client-preset
client-preset already applies the recommended setup, so you won’t have to make any changes to the
default config:
typescript-operations
typescript-operations can be used in a variety of custom setups. This section explains 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 a single file.
Now, you can remove the typescript plugin, as typescript-operations works by itself. It also
only generates Input, Enum, and Operation types that are actually used.
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 reuse the base Input and Enum types generated by the
typescript plugin with the
import-types preset:
Now, it is possible to do this with just typescript-operations, as it supports this approach using
its own generateOperationTypes and importSchemaTypesFrom options:
near-operation-file-preset setups
near-operation-file-preset is commonly used to generate a file next to operation document a.k.a. near-operation file.
Previously, shared types were always generated into a shared file using the typescript plugin, and
reused in every near-operation file:
Now, it is still possible to reuse shared types, but with the decoupling from the typescript
plugin, we can just use typescript-operations for both base type and operation type generation.
Alternatively, base types and operation types can all be generated into every near-operation file:
Breaking changes
1. Object types are no longer generated
Previously, Object types from the schema were generated via the typescript plugin, for example:
These types contain all the fields from the schema. However, GraphQL operations are not expected to fetch all fields, so Object types should never be used. Instead, Operation types (Variables and Result) are generated based on the fields in the documents so these should be used.
In reality, generated Object types were often used (intentionally or accidentally) in application code because they were generated.
Now, Object types are no longer generated. This prevents accidental misuse of schema types in client code and ensures all types accurately reflect actual query selections.
If you need schema types for any reason, please generate them using the typescript plugin in a
separate file.
2. 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.
3. Scalar types are no longer generated as a reusable type
Previously, Scalar types from the schema were generated into an object and reused in Variables types:
Now, scalars in Input and Variables types are consistently inlined (similar to Result types) to
avoid the Scalar utility type:
4. 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 cost (e.g. native TypeScript enum or const enum) are used.
Now, only Input and Enum types used in operations are generated.
5. __typename is only generated when used
Previously, __typename fields in Result types are generated as optional by default, even when they
were not requested:
Previously, the above operation resulted in a type with optional __typename:
Now, __typename is not generated by default when it is not in the selection set:
6. Document field types are generated to correctly match runtime expectations
Previously, nullable fields in Result types were generated as optional by default:
Now, nullable fields in Result types are never optional (except in some cases e.g. when @defer,
@skip, or @include are used)
7. Enum config options are consolidated and the default value is changed
Previously, there were 4 boolean options to set which Enum variant to generate. When combined, these options overrode one another, leading to unexpected and confusing behaviour.
Now, enumType is the only config option to use. The default has also been changed to
string-literal, as it is the only option that does not incur runtime cost.
| Enum type | Examples | Previous config | New config |
|---|---|---|---|
| String literal | type UserRole = 'Admin' | 'Customer' | {enumsAsTypes:true} | {} or {enumType:'string-literal'} |
| Const | export const UserRole = { Admin: 'ADMIN', Customer: 'CUSTOMER' } as const; | {enumsAsConst:true} | {enumType:'const'} |
| Native | export enum UserRole { Admin = 'ADMIN', Customer = 'CUSTOMER' }; | {} or {constEnums:false} | {enumType:'native'} |
| Native const | export const enum UserRole { Admin = 'ADMIN', Customer = 'CUSTOMER' }; | {constEnums:true} | {enumType:'native-const'} |
| Native numeric | export enum UserRole { Admin = 0, Customer = 1 } | {numericEnums:true} | {enumType:'native-numeric'} |
8. avoidOptionals option is updated to only handle Operation types
Previously, avoidOptionals was shared with the
typescript plugin.
As a 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 enabled, each forces the respective use case to pass non-optional values.
avoidOptionals.variableValueavoidOptionals.inputValueavoidOptionals.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.
9. preResolveTypes option is removed
The preResolveTypes option was used to generate Result types inline (preResolveTypes:false) or
use the ones generated by the typescript plugin (preResolveTypes:true). This approach had
several drawbacks:
- it added dependency to the
typescriptplugin trueandfalsehad no functional difference for users- 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
the majority of users by now. So, removing this option is expected to have zero or minimal impact on
users and reduce the maintenance burden.
If you are seeing problems, please create an issue here.
10. 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 type is no longer needed.InputMaybe: used to handle nullability types of Input. Input types are now inlined, so this type is no longer needed.MakeOptional,MakeMaybeandMakeEmpty: used to handlepreResolveTypes:false. However,preResolveTypeshas been removed, so these types are no longer needed.
11. unknown is the default type for custom scalars instead of any
Previously, the default custom Scalars type was any, which bypassed typechecking.
Now, the default type is unknown to ensure data is handled carefully by users.
12. string | number is the default type for the native ID scalar
Previously, one set of default Scalar types 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, the typescript plugin is no longer a dependency. So, we can set the default type as
string | number, which is the correct type for client use cases. For more details on how Scalar
coercion works here, please read
The Complete GraphQL Scalar Guide.
13. client-preset persisted document default hash algorithm is SHA256
Previously, the default hash algorithm for persisted documents was sha1.
Now, the default is sha256. When using sha256, the generated hash conforms to the format defined
in the
GraphQL over HTTP spec for persisted document identifiers.
14. ESM-first
Previously, the Codegen CLI and official plugins were stuck on older dependency versions because ESM-only packages made integration and testing harder for maintainers.
Now that Node.js 20 support has been dropped, Node.js 22 can load ESM seamlessly at runtime with no extra configuration. This allows us to move to ESM across the board:
- Official plugins now use ESM-only dependencies.
- The Codegen CLI is still published in both ESM and CJS formats, with ESM as the default.
Plugin maintainers using Jest may still encounter issues. We recommend switching to Vitest, which has native ESM support.