Skip to content

Commit 3add4f8

Browse files
committed
Add section on providing type overrides
1 parent bf9835d commit 3add4f8

1 file changed

Lines changed: 172 additions & 0 deletions

File tree

docs/source/development-testing/static-typing.mdx

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,178 @@ Variables are validated the same as `useQuery`. See [working with variables](#wo
552552

553553
Learn more about integrating TypeScript with data masking in the [data masking docs](../data/fragments#using-with-typescript).
554554

555+
## Overriding type implementations for built-in types
556+
557+
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.
558+
559+
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.
560+
561+
### Anatomy of HKTs
562+
563+
HKTs in Apollo Client consist of two parts:
564+
565+
- The HKT type definition - This provides the plumbing necessary to use your custom type implementation with Apollo Client
566+
- The `TypeOverrides` interface - An interface used with [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) to provide the mapping for the overridden types to your HKT types
567+
568+
#### Creating an HKT type
569+
570+
You create HKT types by extending the `HKT` interface exported by `@apollo/client/utilities`.
571+
572+
```ts
573+
import { HKT } from "@apollo/client/utilities";
574+
575+
// The implementation of the type
576+
type MyCustomImplementation<GenericArg1, GenericArg2> = SomeOtherUtility<
577+
GenericArg1,
578+
GenericArg2
579+
>;
580+
581+
interface MyTypeOverride extends HKT {
582+
arg1: unknown; // GenericArg1
583+
arg2: unknown; // GenericArg2
584+
return: MyCustomImplementation<this["arg1"], this["arg2"]>;
585+
}
586+
```
587+
588+
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.
589+
590+
#### Mapping HKT types to its type override
591+
592+
Once your HKT type is created, you tell Apollo Client about it by using [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) using the `TypeOverrides` interface. This interface is included in Apollo Client to enable you to provide mappings to your custom type implementations.
593+
594+
Create a TypeScript file that defines the `TypeOverrides` interface for the `@apollo/client` module.
595+
596+
```ts title=apollo-client.d.ts
597+
// This import is necessary to ensure all Apollo Client imports
598+
// are still available to the rest of the application.
599+
import "@apollo/client";
600+
601+
declare module "@apollo/client" {
602+
export interface TypeOverrides {
603+
TypeOverride1: MyTypeOverride;
604+
}
605+
}
606+
```
607+
608+
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.
609+
610+
<Note>
611+
612+
`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](#available-type-overrides) for more information on which types can be overridden.
613+
614+
</Note>
615+
616+
### Example: Custom `dataState` types
617+
618+
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`](#type-narrowing-data-with-datastate) is set to specific values. The `Complete` type is used when `dataState` is `"complete"`, and `Streaming` is used when `dataState` is `"streaming"`.
619+
620+
For this example, we'll assume a custom type generation format where:
621+
622+
- 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.
623+
624+
```ts
625+
type StreamedQuery = {
626+
// The streamed variant of the operation type is provided under the
627+
// `__streaming` virtual property
628+
__streaming?: {
629+
user: { __typename: "User"; id: number } & (
630+
| { name: string }
631+
| { name?: never }
632+
);
633+
};
634+
635+
// The full result type includes all other fields in the type
636+
user: { __typename: "User"; id: number; name: string };
637+
};
638+
```
639+
640+
- Complete types which provide the full type of a query
641+
642+
```ts
643+
type CompleteQuery = {
644+
user: { __typename: "User"; id: number; name: string };
645+
};
646+
```
647+
648+
<Note>
649+
650+
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.
651+
652+
</Note>
653+
654+
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.
655+
656+
```ts title="custom-types.ts"
657+
type Streaming<TData> =
658+
TData extends { __streaming?: infer TStreamingData } ? TStreamingData : TData;
659+
```
660+
661+
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`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys) type.
662+
663+
```ts {4} title="custom-types.ts"
664+
type Streaming<TData> =
665+
TData extends { __streaming?: infer TStreamingData } ? TStreamingData : TData;
666+
667+
type Complete<TData> = Omit<TData, "__streaming">;
668+
```
669+
670+
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`.
671+
672+
Let's provide HKTs for our `Complete` and `Streaming` types. We'll put these in the same file as our type implementations.
673+
674+
```ts {8-11,13-16} title="custom-types.ts"
675+
import { HKT } from "@apollo/client/utilities";
676+
677+
type Streaming<TData> =
678+
TData extends { __streaming?: infer TStreamingData } ? TStreamingData : TData;
679+
680+
type Complete<TData> = Omit<TData, "__streaming">;
681+
682+
export interface StreamingHKT extends HKT {
683+
arg1: unknown; // TData
684+
return: Streaming<this["arg1"]>;
685+
}
686+
687+
export interface CompleteHKT extends HKT {
688+
arg1: unknown; // TData
689+
return: Complete<this["arg1"]>;
690+
}
691+
```
692+
693+
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.
694+
695+
Create a TypeScript file and define a `TypeOverrides` interface for the `@apollo/client` module.
696+
697+
```ts title="apollo-client.d.ts"
698+
// This import is necessary to ensure all Apollo Client imports
699+
// are still available to the rest of the application.
700+
import "@apollo/client";
701+
import { CompleteHKT, StreamingHKT } from "./custom-types";
702+
703+
declare module "@apollo/client" {
704+
export interface TypeOverrides {
705+
Complete: CompleteHKT;
706+
Streaming: StreamingHKT;
707+
}
708+
}
709+
```
710+
711+
And that's it! Now when `dataState` is `"complete"` or `"streaming"`, Apollo Client will use our custom type implementations 🎉.
712+
713+
### Available type overrides
714+
715+
The following utility types are available to override:
716+
717+
- `FragmentType<TFragmentData>` - Type used with fragments to ensure parent objects contain the fragment spread
718+
- `Unmasked<TData>` - Unwraps masked types into the full result type
719+
- `MaybeMasked<TData>` - Conditionally returns either masked or unmasked type
720+
- `Complete<TData>` - Type returned when `dataState` is `"complete"`
721+
- `Streaming<TData>` - Type returned when `dataState` is `"streaming"` (for `@defer` queries)
722+
- `Partial<TData>` - Type returned when `dataState` is `"partial"`
723+
- `AdditionalApolloLinkResultTypes` - Additional types that can be returned from Apollo Link operations
724+
725+
For more information about data masking types specifically, see the [data masking guide](../data/fragments#defining-your-own-masking-types-using-higher-kinded-types).
726+
555727
## Advanced GraphQL Codegen configuration
556728

557729
### Generating relative types files

0 commit comments

Comments
 (0)