TypeScript with Apollo Client


As your application grows, a type system can become an essential tool for catching bugs early and improving your overall developer experience.

GraphQL uses a type system to clearly define the available data for each type and field in a GraphQL schema. Given that a GraphQL server's schema is strongly typed, we can generate TypeScript definitions automatically using a tool like GraphQL Code Generator. We'll use our generated types to ensure type safety for the inputs and results of our GraphQL operations.

We'll guide you through installing and configuring GraphQL Code Generator to generate types for your GraphQL operations.

Setting up your project with GraphQL Codegen

Install the following packages. This installation assumes you already have installed @apollo/client and its dependencies.

Bash
1npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations

Next, we'll create a configuration file for GraphQL Code Generator, named codegen.ts, at the root of our project. The following is a recommended minimal configuration for Apollo Client apps.

TypeScript
codegen.ts
1import { CodegenConfig } from "@graphql-codegen/cli";
2
3const config: CodegenConfig = {
4  overwrite: true,
5  schema: "<URL_OF_YOUR_GRAPHQL_API>",
6  // This assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
7  documents: ["src/**/*.{ts,tsx}"],
8  // Don't exit with non-zero status when there are no documents
9  ignoreNoDocuments: true,
10  generates: {
11    // Use a path that works the best for the structure of your application
12    "./src/types/__generated__/graphql.ts": {
13      plugins: ["typescript", "typescript-operations"],
14      config: {
15        avoidOptionals: {
16          // Use `null` for nullable fields instead of optionals
17          field: true,
18          // Allow nullable input fields to remain unspecified
19          inputValue: false,
20        },
21        // Use `unknown` instead of `any` for unconfigured scalars
22        defaultScalarType: "unknown",
23        // Apollo Client always includes `__typename` fields
24        nonOptionalTypename: true,
25        // Apollo Client doesn't add the `__typename` field to root types so
26        // don't generate a type for the `__typename` for root operation types.
27        skipTypeNameForRoot: true,
28      },
29    },
30  },
31};
32
33export default config;
note
There are multiple ways to specify a schema in your codegen.ts. Use the method that best fits your project's needs.

Finally, we'll add the script to our package.json file:

JSON
package.json
1{
2  "scripts": {
3    "codegen": "graphql-codegen --config codegen.ts"
4  }
5}

Running the script generates types based on the schema file or GraphQL API you provided in codegen.ts:

Bash
$ npm run codegen
 Parse Configuration
 Generate outputs

A note about the client preset

If you follow GraphQL Codegen's quickstart guide, it recommends generating your config file using the GraphQL Code Generator CLI. This wizard installs and configures the @graphql-codegen/client-preset.

We do not recommend using the client preset with Apollo Client apps because it generates additional runtime code that adds bundle size to your application and includes features that are incompatible with Apollo Client. Instead, we recommend using the typescript and typescript-operations plugins directly (at minimum), which focus on only generating types and don't include additional runtime code. Follow the steps in the preceding section to use a setup that includes these plugins.

If you're already using the client preset, or you choose to use it instead of working directly with the plugins, we recommend the following minimal configuration for Apollo Client apps.

TypeScript
1import { CodegenConfig } from "@graphql-codegen/cli";
2
3const config: CodegenConfig = {
4  overwrite: true,
5  schema: "<URL_OF_YOUR_GRAPHQL_API>",
6  // This assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
7  documents: ["src/**/*.{ts,tsx}", "!src/gql/**/*"],
8  // Don't exit with non-zero status when there are no documents
9  ignoreNoDocuments: true,
10  generates: {
11    // Use a path that works the best for the structure of your application
12    "./src/gql/": {
13      preset: "client",
14      presetConfig: {
15        // Disable fragment masking
16        fragmentMasking: false,
17      },
18      config: {
19        avoidOptionals: {
20          // Use `null` for nullable fields instead of optionals
21          field: true,
22          // Allow nullable input fields to remain unspecified
23          inputValue: false,
24        },
25        // Use `unknown` instead of `any` for unconfigured scalars
26        defaultScalarType: "unknown",
27        // Apollo Client always includes `__typename` fields
28        nonOptionalTypename: true,
29        // Apollo Client doesn't add the `__typename` field to root types so
30        // don't generate a type for the `__typename` for root operation types.
31        skipTypeNameForRoot: true,
32      },
33    },
34  },
35};
36
37export default config;
caution
This configuration disables fragment masking in the client preset because it's incompatible with Apollo Client's data masking functionality. If you are using the generated useFragment function or anything generated from the fragment masking feature, you need to migrate away from it to use data masking in Apollo Client. See the guide on data masking for more information on using TypeScript with Apollo Client's data masking feature, including instructions on how to migrate away from GraphQL Codegen's fragment masking feature.

Using operation types

By default, Apollo Client sets the type of the data property to unknown type when the data type cannot be determined. This provides a layer of safety to avoid accessing properties on data that TypeScript doesn't know about.

The following example uses gql to create a GraphQL document for a query. The query is typed as a DocumentNode which doesn't provide type information about its data or variables.

TypeScript
1import { gql } from "@apollo/client";
2import { useQuery } from "@apollo/client/react";
3
4// This is of type `DocumentNode`
5const GET_ROCKET_INVENTORY = gql`
6  query GetRocketInventory {
7    rocketInventory {
8      id
9      model
10      year
11      stock
12    }
13  }
14`;
15
16function RocketInventory() {
17  const { data } = useQuery(GET_ROCKET_INVENTORY);
18  //      ^? unknown
19
20  return (
21    <div>
22      {/* ❌ TypeScript Error: 'data' is of type 'unknown'. */}
23      {data.rocketInventory.map((rocket) => (
24        <Rocket key={rocket.id} rocket={rocket} />
25      ))}
26    </div>
27  );
28}

This however makes it difficult to work with the data property. You either need to type cast each property on data to suppress the error, or cast data as an any type (not recommended) which removes type safety entirely.

Instead, we can leverage the types generated by GraphQL Codegen along with TypedDocumentNode to provide types for GraphQL documents. TypedDocumentNode includes generic arguments for data and variables:

TypeScript
1type QueryType = TypedDocumentNode<Data, Variables>;

Apollo Client allows for the use of TypedDocumentNode everywhere a DocumentNode is accepted. This enables Apollo Client APIs to infer the data and variable types using the GraphQL document.

The following updates the previous example to add types for the query using the types generated from GraphQL Codegen with TypedDocumentNode.

TypeScript
1import { useQuery, TypedDocumentNode } from "@apollo/client";
2import {
3  GetRocketInventoryQuery,
4  GetRocketInventoryQueryVariables,
5} from "@/types/__generated__/graphql";
6
7const GET_ROCKET_INVENTORY: TypedDocumentNode<
8  GetRocketInventoryQuery,
9  GetRocketInventoryQueryVariables
10> = gql`
11  query GetRocketInventory {
12    rocketInventory {
13      id
14      model
15      year
16      stock
17    }
18  }
19`;
20
21function RocketInventory() {
22  const { data } = useQuery(GET_ROCKET_INVENTORY);
23  //      ^? GetRocketInventoryQuery | undefined
24
25  // checks for loading and error states are omitted for brevity
26
27  return (
28    <div>
29      {/* No more 'unknown' type error */}
30      {data.rocketInventory.map((rocket) => (
31        <Rocket key={rocket.id} rocket={rocket} />
32      ))}
33    </div>
34  );
35}
tip
We recommend using TypedDocumentNode and relying on type inference throughout your application for your GraphQL operations instead of specifying the generic type arguments on Apollo Client APIs, such as useQuery. This makes GraphQL documents more portable and provides better type safety wherever the document is used.
Example
TypeScript
1// ❌ Don't leave GraphQL documents as plain `DocumentNode`s
2const query = gql`
3  # ...
4`;
5// ❌ Don't provide generic arguments to Apollo Client APIs
6const { data } = useQuery<QueryType, VariablesType>(query);
7
8// ✅ Add the type for the GraphQL document with `TypedDocumentNode`
9const query: TypedDocumentNode<QueryType, VariablesType> = gql`
10  # ...
11`;
12const { data } = useQuery(query);
tip
We recommend that you always provide the variables type to TypedDocumentNode. GraphQL Codegen creates a variables type, even when the GraphQL operation doesn't define any variables. This ensures you don't provide variable values to your query which results in runtime errors from your GraphQL server. Additionally, this ensures that TypeScript catches errors when you update your query over time and include required variables.

Type narrowing data with dataState

Throughout the lifecycle of your component, the value of data changes. data is influenced by several factors that affect its value such as different options or the query's current state.

Some examples of options that influence the value of data include:

  • returnPartialData which might return partial data from the cache

  • errorPolicy which affects the value of data when an error is returned

  • variables which affects when the query is re-executed as variables change

  • fetchPolicy which might provide a result from the cache

  • skip which waits to execute the query

Some examples of the query state that influence the value of data include:

  • The query is currently loading

  • The query successfully loads its result

  • The query returns partial data from the cache when returnPartialData is true

  • The query returns an error

  • The query is currently streaming additional data while using @defer

The combination of these states and options make it difficult to provide robust types for data based solely on the loading and error properties.

Apollo Client provides a dataState property for this purpose. dataState provides information about the completeness of the data property and includes type narrowing to give you better type safety without the need to add additional completeness checks.

The dataState property has the following values:

  • "empty" - No data is available. data is undefined

  • "partial" - Partial data is returned from the cache. data is DeepPartial<TData>

  • "streaming" - Data from a deferred query is incomplete and still streaming. data is TData

  • "complete" - Data fully satisfies the query. data is TData

The following demonstrates how dataState affects the type of data.

note
This example uses returnPartialData: true to demonstrate the type of data when dataState is partial. When returnPartialData is false or omitted, dataState does not include the partial value and the type of data does not include DeepPartial<TData>.
TypeScript
1const { data, dataState } = useQuery(GET_ROCKET_INVENTORY, {
2  returnPartialData: true,
3});
4
5data;
6// ^? GetRocketInventoryQuery | DeepPartial<GetRocketInventoryQuery> | undefined
7
8if (dataState === "empty") {
9  data;
10  // ^? undefined
11}
12
13if (dataState === "partial") {
14  data;
15  // ^? DeepPartial<GetRocketInventoryQuery>
16}
17
18if (dataState === "streaming") {
19  data;
20  // ^? GetRocketInventoryQuery
21}
22
23if (dataState === "complete") {
24  data;
25  // ^? GetRocketInventoryQuery
26}
note
The type of data is the same when dataState is either streaming or complete. Additionally, dataState always includes streaming as a value, even when @defer is not used and isn't seen at runtime. This is because it is difficult to determine whether a query uses @defer based solely on the output format of the query type generated by GraphQL Codegen.

If you use a type format that makes this possible, you can provide your own type implementations for the complete and streaming states to provide a corrected type. See the guide on overriding types to learn how to provide your own type implementations.

Working with variables

When your GraphQL operations include variables, TypeScript ensures you provide all required variables with the correct types. Additionally, TypeScript ensures you omit variables that aren't included in the operation.

The following adds a non-null variable to the GetRocketInventory query used in previous examples.

TypeScript
1const GET_ROCKET_INVENTORY: TypedDocumentNode<
2  GetRocketInventoryQuery,
3  GetRocketInventoryQueryVariables
4> = gql`
5  query GetRocketInventory($year: Int!) {
6    rocketInventory(year: $year) {
7      id
8      model
9      year
10      stock
11    }
12  }
13`;
14
15function RocketInventory() {
16  // ❌ TypeScript Error: Expected 2 arguments, but got 1.
17  const { data } = useQuery(GET_ROCKET_INVENTORY);
18
19  // ❌ TypeScript Error: Property 'variables' is missing in type '{}'
20  const { data } = useQuery(GET_ROCKET_INVENTORY, {});
21
22  // ❌ TypeScript Error: Property 'year' is missing in type '{}'
23  const { data } = useQuery(GET_ROCKET_INVENTORY, { variables: {} });
24
25  // ✅ Correct: Required variable provided
26  const { data } = useQuery(GET_ROCKET_INVENTORY, {
27    variables: { year: 2024 },
28  });
29
30  // ❌ TypeScript Error: Type 'string' is not assignable to type 'number'
31  const { data } = useQuery(GET_ROCKET_INVENTORY, {
32    variables: { year: "2024" },
33  });
34
35  // ❌ TypeScript Error: 'notAVariable' does not exist in type '{ id: string }'
36  const { data } = useQuery(GET_ROCKET_INVENTORY, {
37    variables: { year: "2024", notAVariable: true },
38  });
39}

For operations with optional variables, TypeScript allows you to omit them:

TypeScript
1const GET_ROCKET_INVENTORY: TypedDocumentNode<
2  GetRocketInventoryQuery,
3  GetRocketInventoryQueryVariables
4> = gql`
5  query GetRocketInventory($model: String, $year: Int) {
6    rocketInventory(model: $model, year: $year) {
7      id
8      model
9    }
10  }
11`;
12
13function RocketInventory() {
14  // ✅ All valid - all variables are optional
15  const { data } = useQuery(GET_ROCKET_INVENTORY);
16
17  // ✅ All valid - All variables satisfy the variables type
18  const { data } = useQuery(GET_ROCKET_INVENTORY, {
19    variables: { model: "Falcon" },
20  });
21
22  // ✅ All valid - All variables satisfy the variables type
23  const { data } = useQuery(GET_ROCKET_INVENTORY, {
24    variables: { year: 2024 },
25  });
26
27  // ✅ All valid - All variables satisfy the variables type
28  const { data } = useQuery(GET_ROCKET_INVENTORY, {
29    variables: { model: "Falcon", year: 2024 },
30  });
31
32  // ❌ TypeScript Error: 'notAVariable' does not exist in type '{ id: string }'
33  const { data } = useQuery(GET_ROCKET_INVENTORY, {
34    variables: { model: "Falcon", year: 2024, notAVariable: true },
35  });
36}

Mutations

For a more comprehensive guide on using mutations, see the Mutations guide.

Like useQuery, you provide useMutation a TypedDocumentNode. This adds the query type for the data property and ensures that variables are validated by TypeScript.

TypeScript
1import { gql, TypedDocumentNode } from "@apollo/client";
2import { useMutation } from "@apollo/client/react";
3import {
4  SaveRocketMutation,
5  SaveRocketMutationVariables,
6} from "@/types/__generated__/graphql";
7
8const SAVE_ROCKET: TypedDocumentNode<
9  SaveRocketMutation,
10  SaveRocketMutationVariables
11> = gql`
12  mutation SaveRocket($rocket: RocketInput!) {
13    saveRocket(rocket: $rocket) {
14      model
15    }
16  }
17`;
18
19export function NewRocketForm() {
20  const [model, setModel] = useState("");
21  const [year, setYear] = useState(0);
22  const [stock, setStock] = useState(0);
23
24  const [addRocket, { data }] = useMutation(SAVE_ROCKET, {
25    //                ^? SaveRocketMutation | null | undefined
26    variables: { rocket: { model, year, stock } },
27  });
28
29  return (
30    <form>
31      <p>
32        <label>Model</label>
33        <input name="model" onChange={(e) => setModel(e.target.value)} />
34      </p>
35      <p>
36        <label>Year</label>
37        <input
38          type="number"
39          name="year"
40          onChange={(e) => setYear(+e.target.value)}
41        />
42      </p>
43      <p>
44        <label>Stock</label>
45        <input
46          type="number"
47          name="stock"
48          onChange={(e) => setStock(e.target.value)}
49        />
50      </p>
51      <button onClick={() => addRocket()}>Add rocket</button>
52    </form>
53  );
54}
note
Unlike useQuery, useMutation doesn't provide a dataState property. data isn't tracked the same way as useQuery because its value is not read from the cache. The value of data is a set result of the last execution of the mutation.

Using variables with useMutation

useMutation allows you to provide variables to either the hook or the mutate function returned in the result tuple. When variables are provided to both the hook and mutate function, they are shallowly merged (see the Mutations guide for more information).

This behavior affects how TypeScript checks required variables with useMutation. Required variables must be provided to either the hook or the mutate function. If a required variable is not provided to the hook, the mutate function must include it. Required variables provided to the hook make them optional in the mutate function.

The following uses the previous example but flattens the variable declarations to demonstrate how required variables affect TypeScript validation.

TypeScript
1const SAVE_ROCKET: TypedDocumentNode<
2  SaveRocketMutation,
3  SaveRocketMutationVariables
4> = gql`
5  mutation SaveRocket($model: String!, $year: Int!, $stock: Int) {
6    saveRocket(rocket: { model: $model, year: $year, stock: $stock }) {
7      model
8    }
9  }
10`;
11
12export function NewRocketForm() {
13  // No required variables provided to the hook
14  const [addRocket] = useMutation(SAVE_ROCKET);
15
16  // ❌ TypeScript Error: Expected 1 argument, but got 0.
17  addRocket();
18  // ❌ TypeScript Error: Property 'variables' is missing in type '{}'
19  addRocket({});
20  // ❌ TypeScript Error: Type '{}' is missing the following properties from '{ year: number; model: string;, stock?: number }': model, year
21  addRocket({ variables: {} });
22  // ❌ TypeScript Error: Property 'year' is missing in type '{ model: string }'
23  addRocket({ variables: { model: "Falcon" } });
24  // ✅ Correct: All required variables provided
25  addRocket({ variables: { model: "Falcon", year: 2025 } });
26  // ❌ TypeScript Error: 'notAVariable' does not exist in type '{ year: number; model: string;, stock?: number }'
27  addRocket({ variables: { model: "Falcon", year: 2025, notAVariable: true } });
28
29  // Some required variables provided to the hook
30  const [addRocket] = useMutation(SAVE_ROCKET, {
31    variables: { model: "Falcon" },
32  });
33
34  // ❌ TypeScript Error: Expected 1 argument, but got 0.
35  addRocket();
36  // ❌ TypeScript Error: Property 'variables' is missing in type '{}'
37  addRocket({});
38  // ❌ TypeScript Error: Property 'year' is missing in type '{}'
39  addRocket({ variables: {} });
40  // ✅ Correct: All remaining required variables provided
41  addRocket({ variables: { year: 2025 } });
42  // ❌ TypeScript Error: 'notAVariable' does not exist in type '{ year: number; model?: string;, stock?: number }'
43  addRocket({ variables: { year: 2025, notAVariable: true } });
44
45  // All required variables provided to the hook
46  const [addRocket] = useMutation(SAVE_ROCKET, {
47    variables: { model: "Falcon", year: 2025 },
48  });
49
50  // ✅ Correct: All required variables are provided to the hook
51  addRocket();
52  // ✅ Correct: All required variables are provided to the hook
53  addRocket({ variables: { stock: 10 } });
54  // ❌ TypeScript Error: 'notAVariable' does not exist in type '{ year?: number; model?: string;, stock?: number }'
55  addRocket({ variables: { notAVariable: true } });
56}

Subscriptions

For a more comprehensive guide on using subscriptions, see the Subscriptions guide.

Like useQuery, you provide useSubscription a TypedDocumentNode. This adds the query type for the data property and ensures that variables are validated by TypeScript.

TypeScript
1import { gql, TypedDocumentNode } from "@apollo/client";
2import { useSubscription } from "@apollo/client/react";
3import {
4  GetLatestNewsSubscription,
5  GetLatestNewsSubscriptionVariables,
6} from "@/types/__generated__/graphql";
7
8const LATEST_NEWS: TypedDocumentNode<
9  GetLatestNewsSubscription,
10  GetLatestNewsSubscriptionVariables
11> = gql`
12  subscription GetLatestNews {
13    latestNews {
14      content
15    }
16  }
17`;
18
19export function LatestNews() {
20  const { data } = useSubscription(LATEST_NEWS);
21  //      ^? GetLatestNewsSubscription | undefined
22
23  return (
24    <div>
25      <h5>Latest News</h5>
26      <p>{data?.latestNews.content}</p>
27    </div>
28  );
29}

Variables are validated the same as useQuery. See working with variables to learn more about how required variables are validated.

Defining context types

Apollo Client enables you to pass custom context through to the link chain. By default, context is typed as Record<string, any> to allow for any arbitrary value as context. However, this default has a few downsides. You don't get proper type safety and you don't get autocomplete suggestions in your editor to understand what context options are available to you.

You can define your own context types for better type safety in Apollo Client using TypeScript's declaration merging.

Adding custom context properties

To define types for your custom context properties, create a TypeScript file and define the DefaultContext interface.

TypeScript
apollo-client.d.ts
1// This import is necessary to ensure all Apollo Client imports
2// are still available to the rest of the application.
3import "@apollo/client";
4
5declare module "@apollo/client" {
6  interface DefaultContext {
7    myProperty?: string;
8    requestId?: number;
9  }
10}

Now when you pass context for operations, TypeScript will validate the types:

TypeScript
1const { data } = useQuery(MY_QUERY, {
2  context: {
3    myProperty: "value", // ✅ Valid value
4    requestId: "123", // ❌ Type 'string' is not assignable to type 'number | undefined'
5  },
6});
tip
We recommend all custom context properties are optional by default. If a context value is necessary for the operation of your application (such as an authentication token used in request headers), we recommend that you try to provide the value with SetContextLink or a custom link first. Reserve required properties for options that are truly meant to be provided to every hook or request to your server and cannot be provided by a link in your link chain.
note
Apollo Client doesn't validate that the context option is provided when it contains required properties like it does with the variables option. However, adding the context option might result in a TypeScript error when any required properties are not provided.

Some links provided by Apollo Client have built-in context option types. You can extend DefaultContext with these types to get proper type checking for link-specific options.

The following example adds HttpLink's context types to DefaultContext:

TypeScript
apollo-client.d.ts
1import "@apollo/client";
2import { HttpLink } from "@apollo/client";
3
4declare module "@apollo/client" {
5  interface DefaultContext extends HttpLink.ContextOptions {}
6}

Now when you pass context for operations, TypeScript will validate the types:

TypeScript
1const { data } = useQuery(MY_QUERY, {
2  context: {
3    headers: {
4      "X-Custom-Header": "value", // ✅ Valid header value
5    },
6    credentials: "include", // ✅ Valid RequestCredentials value
7    fetchOptions: {
8      mode: "none", // ❌ Type "none" is not assignable to type 'RequestMode | undefined'
9    },
10  },
11});

If you are using more than one link that has context options, you can extend from each link's context options:

TypeScript
apollo-client.d.ts
1import "@apollo/client";
2import type { BaseHttpLink } from "@apollo/client/link/http";
3import type { ClientAwarenessLink } from "@apollo/client/link/client-awareness";
4
5declare module "@apollo/client" {
6  interface DefaultContext
7    extends BaseHttpLink.ContextOptions,
8      ClientAwarenessLink.ContextOptions {}
9}

The following built-in links provide context option types:

With context types defined, context properties accessed in custom links are properly type checked when reading or writing context.

TypeScript
1import { ApolloLink } from "@apollo/client";
2
3const customLink = new ApolloLink((operation, forward) => {
4  const context = operation.getContext();
5
6  // TypeScript knows about your custom properties
7  console.log(context.myProperty); // string | undefined
8  console.log(context.requestId); // number | undefined
9
10  operation.setContext({
11    requestId: "123", // ❌ Type 'string' is not assignable to type 'number | undefined'
12  });
13
14  return forward(operation);
15});

Data masking

Learn more about integrating TypeScript with data masking in the data masking docs.

@defer

Learn more about integrating TypeScript with incremental responses the @defer docs.

Overriding type implementations for built-in types

Apollo Client makes it possible to use custom implementations of certain built-in utility types. This enables you to work with custom type outputs that might otherwise be incompatible with the default type implementations in Apollo Client.

You use a technique called higher-kinded types (HKT) to provide your own type implementations for Apollo Client's utility types. You can think of higher-kinded types as a way to define types and interfaces with generics that can be filled in by Apollo Client internals at a later time. Passing around un-evaluated types is otherwise not possible in TypeScript.

Anatomy of HKTs

HKTs in Apollo Client consist of two parts:

  • The HKT type definition - This provides the plumbing necessary to use your custom type implementation with Apollo Client

  • The TypeOverrides interface - An interface used with declaration merging to provide the mapping for the overridden types to your HKT types

Creating an HKT type

You create HKT types by extending the HKT interface exported by @apollo/client/utilities.

TypeScript
1import { HKT } from "@apollo/client/utilities";
2
3// The implementation of the type
4type MyCustomImplementation<GenericArg1, GenericArg2> = SomeOtherUtility<
5  GenericArg1,
6  GenericArg2
7>;
8
9interface MyTypeOverride extends HKT {
10  arg1: unknown; // GenericArg1
11  arg2: unknown; // GenericArg2
12  return: MyCustomImplementation<this["arg1"], this["arg2"]>;
13}

You can think of each property on the HKT type as a placeholder. Each arg* property corresponds to a generic argument used for the implementation. The return property provides the mapping to the actual implementation of the type, using the arg* values as generic arguments.

Mapping HKT types to its type override

Once your HKT type is created, you tell Apollo Client about it by using declaration merging using the TypeOverrides interface. This interface is included in Apollo Client to enable you to provide mappings to your custom type implementations.

Create a TypeScript file that defines the TypeOverrides interface for the @apollo/client module.

TypeScript
apollo-client.d.ts
1// This import is necessary to ensure all Apollo Client imports
2// are still available to the rest of the application.
3import "@apollo/client";
4
5declare module "@apollo/client" {
6  export interface TypeOverrides {
7    TypeOverride1: MyTypeOverride;
8  }
9}

Each key in the TypeOverrides interface corresponds to an overridable type in Apollo Client and its value maps to an HKT type that provides the definition for that type.

note
TypeOverride1 is used as an example in the previous code block but is not a valid overridable type so it is ignored. See the available type overrides for more information on which types can be overridden.

Example: Custom dataState types

Let's add our own type overrides for the Complete and Streaming utility types. These types are used to provide types for the data property when dataState is set to specific values. The Complete type is used when dataState is "complete", and Streaming is used when dataState is "streaming".

For this example, we'll assume a custom type generation format where:

  • Streamed types (i.e. operation types that use the @defer directive) include a __streaming virtual property. Its value is the operation type that should be used when the result is still streaming from the server.

    TypeScript
    1type StreamedQuery = {
    2  // The streamed variant of the operation type is provided under the
    3  // `__streaming` virtual property
    4  __streaming?: {
    5    user: { __typename: "User"; id: number } & (
    6      | { name: string }
    7      | { name?: never }
    8    );
    9  };
    10
    11  // The full result type includes all other fields in the type
    12  user: { __typename: "User"; id: number; name: string };
    13};
  • Complete types which provide the full type of a query

    TypeScript
    1type CompleteQuery = {
    2  user: { __typename: "User"; id: number; name: string };
    3};
note
This is a hypothetical format that doesn't exist in Apollo Client or any known code generation tool. This format is used specifically for this example to illustrate how to provide type overrides to Apollo Client.

First, let's define our custom implementation of the Streaming type. The implementation works by checking if the __streaming virtual property exists on the type. If so, it returns the value on the __streaming property as the type, otherwise it returns the input type unmodified.

TypeScript
custom-types.ts
1type Streaming<TData> =
2  TData extends { __streaming?: infer TStreamingData } ? TStreamingData : TData;

Now let's define our custom implementation of the Complete type. The implementation works by removing the __streaming virtual property on the input type. This can be accomplished using the built-in Omit type.

TypeScript
custom-types.ts
1type Streaming<TData> =
2  TData extends { __streaming?: infer TStreamingData } ? TStreamingData : TData;
3
4type Complete<TData> = Omit<TData, "__streaming">;

Now we need to define higher-kinded types for each of these implementations. This provides the bridge needed by Apollo Client to use our custom type implementations. This is done by extending the HKT interface exported by @apollo/client/utilities.

Let's provide HKTs for our Complete and Streaming types. We'll put these in the same file as our type implementations.

TypeScript
custom-types.ts
1import { HKT } from "@apollo/client/utilities";
2
3type Streaming<TData> =
4  TData extends { __streaming?: infer TStreamingData } ? TStreamingData : TData;
5
6type Complete<TData> = Omit<TData, "__streaming">;
7
8export interface StreamingHKT extends HKT {
9  arg1: unknown; // TData
10  return: Streaming<this["arg1"]>;
11}
12
13export interface CompleteHKT extends HKT {
14  arg1: unknown; // TData
15  return: Complete<this["arg1"]>;
16}

With our HKT types in place, we now need to tell Apollo Client about them. We'll need to provide our type overrides on the TypeOverrides interface.

Create a TypeScript file and define a TypeOverrides interface for the @apollo/client module.

TypeScript
apollo-client.d.ts
1// This import is necessary to ensure all Apollo Client imports
2// are still available to the rest of the application.
3import "@apollo/client";
4import { CompleteHKT, StreamingHKT } from "./custom-types";
5
6declare module "@apollo/client" {
7  export interface TypeOverrides {
8    Complete: CompleteHKT;
9    Streaming: StreamingHKT;
10  }
11}

And that's it! Now when dataState is "complete" or "streaming", Apollo Client will use our custom type implementations 🎉.

Available type overrides

The following utility types are available to override:

  • FragmentType<TFragmentData> - Type used with fragments to ensure parent objects contain the fragment spread

  • Unmasked<TData> - Unwraps masked types into the full result type

  • MaybeMasked<TData> - Conditionally returns either masked or unmasked type

  • Complete<TData> - Type returned when dataState is "complete"

  • Streaming<TData> - Type returned when dataState is "streaming" (for @defer queries)

  • Partial<TData> - Type returned when dataState is "partial"

  • AdditionalApolloLinkResultTypes<TData, TExtensions> - Additional types that can be returned from Apollo Link operations

For more information about data masking types specifically, see the data masking guide.

Advanced GraphQL Codegen configuration

Generating relative types files

As your application scales, a single types file that contains all operation types might become unwieldy. The near-operation-file-preset makes it possible to generate an operation types file relative to the file where the operation is defined.

Bash
1npm install -D @graphql-codegen/near-operation-file-preset

The following is a recommended minimal configuration. See the near-operation-file-preset documentation for additional configuration options, such as customizing the file name or extension.

TypeScript
codegen.ts
1import { CodegenConfig } from "@graphql-codegen/cli";
2
3const config: CodegenConfig = {
4  overwrite: true,
5  schema: "<URL_OF_YOUR_GRAPHQL_API>",
6  // This assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
7  documents: ["src/**/*.{ts,tsx}"],
8  // Don't exit with non-zero status when there are no documents
9  ignoreNoDocuments: true,
10  generates: {
11    "./src/types/__generated__/graphql.ts": {
12      plugins: ["typescript"],
13    },
14    "./src/": {
15      preset: "near-operation-file",
16      presetConfig: {
17        // This should be the file generated by the "typescript" plugin above,
18        // relative to the directory specified for this configuration
19        baseTypesPath: "./types/__generated__/graphql.ts",
20      },
21      plugins: ["typescript-operations"],
22      // Note: these config options moved from the other generated file config
23      config: {
24        avoidOptionals: {
25          // Use `null` for nullable fields instead of optionals
26          field: true,
27          // Allow nullable input fields to remain unspecified
28          inputValue: false,
29        },
30        // Use `unknown` instead of `any` for unconfigured scalars
31        defaultScalarType: "unknown",
32        // Apollo Client always includes `__typename` fields
33        nonOptionalTypename: true,
34        // Apollo Client doesn't add the `__typename` field to root types so
35        // don't generate a type for the `__typename` for root operation types.
36        skipTypeNameForRoot: true,
37      },
38    },
39  },
40};
41
42export default config;

Usage

The following uses the example from the TypedDocumentNode section.

TypeScript
my-component.ts
1import { useQuery, TypedDocumentNode } from "@apollo/client/react";
2// The query name and path might differ depending on your codegen config
3import {
4  GetRocketInventoryQuery,
5  GetRocketInventoryQueryVariables,
6} from "./my-component.generated";
7
8const GET_ROCKET_INVENTORY: TypedDocumentNode<
9  GetRocketInventoryQuery,
10  GetRocketInventoryQueryVariables
11> = gql`
12  query GetRocketInventory($year: Int!) {
13    rocketInventory(year: $year) {
14      id
15      model
16      year
17      stock
18    }
19  }
20`;
21
22function MyComponent() {
23  const { data } = useQuery(GET_ROCKET_INVENTORY, {
24    //    ^? GetRocketInventoryQuery | undefined;
25    variables: { year: 2025 },
26  });
27
28  // ...
29}

Generating precompiled GraphQL documents with their type definitions

The gql template literal tag is used in Apollo Client apps to define GraphQL documents for use with Apollo Client APIs. Its purpose is to parse the GraphQL string into a standard GraphQL AST. Parsing occurs when the module is executed which adds startup time to your application. Additionally, the GraphQL document returned by gql is typed as a DocumentNode which doesn't include type information about its data or variables.

The typed-document-node plugin makes it possible to generate precompiled GraphQL documents preconfigured with TypedDocumentNode.

Bash
1npm install -D @graphql-codegen/typed-document-node

The following is a recommended minimal configuration which uses the near-operation-file-preset configuration from the previous section. If you don't use near-operation-file-preset, add the plugin to the file config that uses the typescript-operations plugin.

TypeScript
codegen.ts
1import { CodegenConfig } from "@graphql-codegen/cli";
2
3const config: CodegenConfig = {
4  overwrite: true,
5  schema: "<URL_OF_YOUR_GRAPHQL_API>",
6  // This assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
7  documents: ["src/**/*.{ts,tsx}"],
8  // Don't exit with non-zero status when there are no documents
9  ignoreNoDocuments: true,
10  generates: {
11    "./src/types/__generated__/graphql.ts": {
12      plugins: ["typescript"],
13    },
14    "./src/": {
15      preset: "near-operation-file",
16      presetConfig: {
17        // This should be the file generated by the "typescript" plugin above,
18        // relative to the directory specified for this configuration
19        baseTypesPath: "./types/__generated__/graphql.ts",
20      },
21      plugins: ["typescript-operations", "typed-document-node"],
22      // Note: these config options moved from the other generated file config
23      config: {
24        avoidOptionals: {
25          // Use `null` for nullable fields instead of optionals
26          field: true,
27          // Allow nullable input fields to remain unspecified
28          inputValue: false,
29        },
30        // Use `unknown` instead of `any` for unconfigured scalars
31        defaultScalarType: "unknown",
32        // Apollo Client always includes `__typename` fields
33        nonOptionalTypename: true,
34        // Apollo Client doesn't add the `__typename` field to root types so
35        // don't generate a type for the `__typename` for root operation types.
36        skipTypeNameForRoot: true,
37      },
38    },
39  },
40};
41
42export default config;
note
You might need to change the structure of your application to avoid bundling the query more than once in your application. If you author GraphQL documents using the gql template tag, you won't consume the returned GraphQL document since you'll import and use the precompiled document generated by this plugin instead.

You may need to tweak your bundler settings to strip out unused variables from your bundle. Alternatively you can author GraphQL operations in .graphql files.

Usage

The following uses the example from the TypedDocumentNode section.

TypeScript
my-component.ts
1import { useQuery } from "@apollo/client/react";
2// The query name and path might differ depending on your codegen config
3import { getRocketInventoryQuery } from "./my-component.generated";
4
5function MyComponent() {
6  const { data } = useQuery(getRocketInventoryQuery, {
7    //    ^? GetRocketInventoryQuery | undefined;
8    variables: { year: 2025 },
9  });
10
11  // ...
12}
Feedback

Edit on GitHub

Ask Community