TypeScript ​
GraphQL's type system pairs naturally with TypeScript. Together they give you end-to-end type safety from your schema to your Vue components.
GraphQL Codegen (recommended) ​
GraphQL Code Generator with the client preset generates TypeScript types directly from your schema. It works seamlessly with Apollo Client's data masking feature.
Installation ​
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
npm install @graphql-typed-document-node/corepnpm add -D @graphql-codegen/cli @graphql-codegen/client-preset
pnpm add @graphql-typed-document-node/coreyarn add -D @graphql-codegen/cli @graphql-codegen/client-preset
yarn add @graphql-typed-document-node/coreConfiguration ​
Create codegen.ts at the root of your project:
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: '<URL_OR_PATH_TO_YOUR_SCHEMA>',
documents: 'src/**/*.vue',
importExtension: '.ts',
generates: {
'src/graphql/': {
preset: 'client',
presetConfig: {
fragmentMasking: false,
},
config: {
customDirectives: {
apolloUnmask: true,
},
inlineFragmentTypes: 'mask',
useTypeImports: true,
enumAsConst: true,
},
},
},
}
export default configAdd the script to your package.json:
{
"scripts": {
"codegen": "graphql-codegen"
}
}Run codegen:
npm run codegenpnpm codegenyarn codegenWatch mode ​
To regenerate types whenever your GraphQL documents change:
npm run codegen -- --watchpnpm codegen --watchyarn codegen --watchWatch mode needs @parcel/watcher as a dev dependency:
npm install -D @parcel/watcherpnpm add -D @parcel/watcheryarn add -D @parcel/watcherUsage ​
Import the graphql function from the generated output and define your queries inline:
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import { graphql } from '@/graphql'
const { projectId } = defineProps<{ projectId: string }>()
const { current } = useQuery(graphql(`
query GetProject($projectId: ID!) {
project(id: $projectId) {
__typename
id
name
tasks {
__typename
id
title
...TaskCard
}
}
}
`), {
variables: {
projectId: () => projectId,
},
})
</script>
<template>
<div v-if="current.loading">
Loading...
</div>
<div v-else-if="current.error">
Error: {{ current.error.message }}
</div>
<div v-else-if="current.resultState === 'complete'">
<h1>{{ current.result.project.name }}</h1>
<TaskCard
v-for="task in current.result.project.tasks"
:key="task.id"
:task="task"
/>
</div>
</template>The graphql() function:
- Parses your GraphQL document at build time.
- Returns a
TypedDocumentNodewith full type inference. - Automatically includes fragment definitions.
Enabling data masking types ​
By default, Apollo Client does not modify operation types regardless of whether they are masked or unmasked. To make GraphQL Codegen's masking types match runtime behavior, augment Apollo Client's TypeOverrides:
import type { GraphQLCodegenDataMasking } from '@apollo/client/masking'
// This import keeps the rest of @apollo/client's types available.
import '@apollo/client'
declare module '@apollo/client' {
interface TypeOverrides extends GraphQLCodegenDataMasking.TypeOverrides {}
}Place this file (for example apollo-client.d.ts) somewhere your tsconfig.json include covers. With the augmentation in place:
- Masked types omit fields from spread fragments.
- The
@unmaskdirective correctly unmasks types. FragmentTypeworks for type-safe fragment props.
Type-safe fragments ​
Use FragmentType from @apollo/client to type component props that receive fragment data:
<script setup lang="ts">
import type { FragmentType } from '@apollo/client'
import { useFragment } from '@vue/apollo-composable'
import { graphql } from '@/graphql'
const { task } = defineProps<{
task: FragmentType<typeof TaskCardFragment>
}>()
const TaskCardFragment = graphql(`
fragment TaskCard on Task {
__typename
id
title
status
assignee {
name
}
}
`)
const { current } = useFragment(() => ({
fragment: TaskCardFragment,
from: () => task,
}))
</script>
<template>
<div v-if="current.complete" class="task-card">
<h3>{{ current.result.title }}</h3>
<span class="status">{{ current.result.status }}</span>
<span v-if="current.result.assignee" class="assignee">
{{ current.result.assignee.name }}
</span>
</div>
</template>This pattern:
- Uses
FragmentTypeso the parent must pass a correctly-typed fragment reference. - Uses
useFragmentto read the fragment from the cache. - Works with Data Masking for isolated component data.
Composable return-value shapes ​
Vue Apollo's composables expose different result shapes depending on what makes sense for each operation. The table below shows what each composable returns:
| Composable | current ref (discriminated union) | Individual refs |
|---|---|---|
useQuery | result, resultState, loading, networkStatus, error, partial | result, loading, networkStatus, error |
useLazyQuery | Same as useQuery (inherits) | Same as useQuery, plus load() |
useFragment | result, resultState, complete, missing | result, resultState, complete, missing |
useMutation | not provided | result, loading, called, error |
useSubscription | not provided | result, loading, error, variables |
For useQuery, useLazyQuery, and useFragment we recommend reading from current because the resultState discriminator narrows the type of result:
const { current } = useQuery(GET_USERS)
if (current.value.resultState === 'complete') {
// current.value.result is now fully typed as TData
console.log(current.value.result.users)
}The individual refs remain available for code that only reads loading or error without accessing result.
For useMutation and useSubscription the discriminated union is not provided because their results do not have multiple data states. A mutation is request-response, and a subscription delivers one result at a time.
Type narrowing with resultState ​
The resultState discriminator narrows result precisely. The states for useQuery:
| State | Description | result Type |
|---|---|---|
'complete' | Data fully satisfies the query | TData |
'partial' | Partial data from cache (with returnPartialData: true) | DeepPartial<TData> |
'streaming' | Data streaming via @defer or @stream | TData |
'empty' | No data yet | undefined |
const { current } = useQuery(GET_USERS, { returnPartialData: true })
if (current.value.resultState === 'complete') {
// TData
}
else if (current.value.resultState === 'partial') {
// DeepPartial<TData>
}
else if (current.value.resultState === 'streaming') {
// TData (still arriving)
}For useFragment the states are 'complete' and 'partial', and follow the same narrowing pattern.
Working with variables ​
TypeScript validates required variables and their types:
// TypeScript Error: Property 'variables' is missing
const { current } = useQuery(GET_USER_QUERY)
// TypeScript Error: Property 'id' is missing
const { current } = useQuery(GET_USER_QUERY, { variables: {} })
// OK
const { current } = useQuery(GET_USER_QUERY, {
variables: { id: '1' },
})When variables are entirely optional (the query has no required variables), the variables option itself is optional.
Manual TypedDocumentNode ​
If you do not use GraphQL Codegen, you can type documents manually with TypedDocumentNode:
import type { TypedDocumentNode } from '@apollo/client'
import { gql } from '@apollo/client'
interface GetUsersQuery {
users: Array<{ id: string, name: string }>
}
type GetUsersVariables = Record<string, never>
const GET_USERS: TypedDocumentNode<GetUsersQuery, GetUsersVariables> = gql`
query GetUsers {
users { id name }
}
`TIP
Always provide the variables type. For queries with no variables, use Record<string, never> so accidental variables produce a type error.
Next steps ​
- Queries for type-safe queries.
- Mutations for type-safe mutations.
- Fragments for colocated fragment types.
- Data Masking for isolated component data.