Using GraphQL Generated Types
This is where typed TypeScript provides the most value for regression prevention. Every test that provides mock GraphQL data should type that data against the generated query/mutation types.
Typing Mock Responses
Wrap mock handler responses in the generated query type:
import type { GetSubmissionQuery } from "src/graphql/generated/graphql"
import { SubmissionStatus } from "src/graphql/generated/graphql"
const mockResponse: { data: GetSubmissionQuery } = {
data: {
submission: {
__typename: "Submission",
id: "1",
title: "Test Submission",
status: SubmissionStatus.INITIALLY_SUBMITTED,
// ... all fields required by the query
}
}
}
handler.mockResolvedValue(mockResponse)What this catches: If the backend renames title to name, or adds a required field, or changes the status enum values, the test fails at compile time -- not silently at runtime with wrong data.
Deriving Types from Queries
When you need the type of a nested object (e.g., a single submission from a list), derive it from the query type rather than writing a parallel interface:
import type { CurrentUserSubmissionsQuery } from "src/graphql/generated/graphql"
type SubmissionData = NonNullable<
CurrentUserSubmissionsQuery["currentUser"]
>["submissions"][number]NonNullable<> strips the null | undefined from nullable fields (codegen generates Maybe<T> which is T | null | undefined). The [number] index extracts the element type from an array.
This keeps test helper types automatically in sync with the schema -- no manual updates needed when fields change.
Using Generated Enums
Always use the generated enum values instead of magic strings:
import { SubmissionStatus } from "src/graphql/generated/graphql"
// ✅ Compile error if the enum value is renamed or removed
mockSubmission("100", SubmissionStatus.UNDER_REVIEW, "submitter")
// ❌ Silent breakage if the backend changes the value
mockSubmission("100", "UNDER_REVIEW", "submitter")Boundary Casting for Partial Mocks
Tests often don't need every field of a large generated type. Cast at the boundary (the object literal) with a specific type, not as any:
// ✅ TypeScript still checks the fields you provide
const props = {
container: {
__typename: "Publication",
id: "1",
editors: [
{ id: "1", email: "test@example.com", name: "TestUser" } as User,
]
} as Publication
}
// ❌ No type checking at all
const props = {
container: { id: "1", editors: [...] } as any
}as Publication tells TypeScript "trust me, this satisfies Publication" while still checking the fields you do provide against the type. as any disables all checking entirely.
When to use as any
as any is a last resort. The one common case where it's unavoidable is wrapper.vm as any when accessing <script setup> internals from test-utils, because Vue does not expose those types. Always prefer a narrower cast first.