
How to Validate Hey API Generated Types with typedStruct
Keep generated TypeScript types as the source of truth while adding small runtime guards at the API boundaries that need them.
The quick answer
Import the generated type into a file outside the Hey API output directory, then pass it to typedStruct<T>(). The resulting guard is checked against that generated object shape.
// src/client/types.gen.ts// Generated by Hey API. Do not edit this file.export type Pet = {id?: number;name: string;};
import type { Pet } from '@/client/types.gen';import {isNumber,isString,optionalKey,safeParse,typedStruct,} from 'is-kit';const isPet = typedStruct<Pet>()({id: optionalKey(isNumber),name: isString,});async function readPet(response: Response) {const payload: unknown = await response.json();const parsed = safeParse(isPet, payload);if (!parsed.valid) {throw new Error('Invalid Pet response');}return parsed.value;}
Keep the generated type and the hand-written runtime check connected. When regeneration changes a field, TypeScript makes the guard file fail until its validation policy is updated.
Generated TypeScript is not runtime validation
Hey API's TypeScript plugin writes interfaces and type aliases to types.gen.ts. Those types are erased when the application is built, so they cannot prove that a server, proxy, cache, fixture, or persisted payload actually returned the documented shape.
Hey API can also generate and integrate runtime validators. Validation is not enabled by default because it adds runtime work. Use typedStruct when you want a small hand-written guard for selected boundaries rather than generated validation across the SDK.
See Hey API's official TypeScript plugin documentation and SDK validator configuration for the generated alternatives.
Keep guards outside the generated directory
Treat Hey API's output folder like a dependency. Regeneration may replace its contents, so place guards in an application-owned module such as src/validation/pet.ts and import only the generated types.
src/client/types.gen.tsremains generated.src/validation/pet.tsowns the runtime policy.- Application code imports the guard instead of editing generated output.
Model optional and nullable fields separately
OpenAPI distinguishes a missing property from a property whose value is null. Preserve that distinction in the guard instead of treating both cases as one kind of absence.
import type { PetDetails } from '@/client/types.gen';import {isNumber,isString,nullable,optionalKey,typedStruct,} from 'is-kit';// Generated shape:// type PetDetails = {// id?: number;// nickname: string | null;// notes?: string | null;// };const isPetDetails = typedStruct<PetDetails>()({id: optionalKey(isNumber),nickname: nullable(isString),notes: optionalKey(nullable(isString)),});
| Generated field | Guard field | Accepted runtime value |
|---|---|---|
name: string | isString | An own string property |
id?: number | optionalKey(isNumber) | A missing key or a number |
nickname: string | null | nullable(isString) | A present string or null |
notes?: string | null | optionalKey(nullable(isString)) | A missing key, string, or null |
Compose generated definitions
Reusable OpenAPI definitions usually become reusable generated types. Build their guards the same way, then compose the smaller guard into the response guard.
import type {Owner,PetWithOwner,} from '@/client/types.gen';import {isNumber,isString,nullable,typedStruct,} from 'is-kit';const isOwner = typedStruct<Owner>()({id: isNumber,name: isString,});const isPetWithOwner = typedStruct<PetWithOwner>()({id: isNumber,owner: nullable(isOwner),});
If Owner changes after regeneration, isOwner becomes the single place that must adopt the new runtime policy.
Match mutable generated array fields
Hey API may represent collection properties as mutable Array<T> types. arrayOf intentionally narrows to a readonly array, so wrap the same runtime check with define when the field guard must match a generated mutable array type.
import type { PetWithTags } from '@/client/types.gen';import {arrayOf,define,isString,optionalKey,typedStruct,} from 'is-kit';// PetWithTags['tags'] is Array<string> | undefined.const isPetTags = define<NonNullable<PetWithTags['tags']>>(arrayOf(isString),);const isPetWithTags = typedStruct<PetWithTags>()({name: isString,tags: optionalKey(isPetTags),});
The wrapper changes the TypeScript contract to match the generated field. It does not change the runtime check: the value must still be an array and every element must pass isString.Let regeneration expose drift
typedStruct requires every required and optional string-keyed property from the generated type to appear in the guard schema. If the OpenAPI document adds, removes, or changes a field, regenerating the client produces a compile-time error in the guard instead of silently leaving the runtime check stale.
Numeric and symbol properties are outside this contract. That is normally a natural fit for JSON object models, whose property names are strings.
Choose extra-key behavior deliberately
By default, typedStruct validates declared fields and allows additional own enumerable string keys. This is often the safer response-boundary default because a server can add a field without breaking an older client.
import type { Pet } from '@/client/types.gen';import {isNumber,isString,optionalKey,typedStruct,} from 'is-kit';const isExactPet = typedStruct<Pet>()({id: optionalKey(isNumber),name: isString,},{ exact: true },);
Use exact: true only when extra fields are invalid for the boundary itself, such as a strict fixture or a signed payload.
typedStruct or a generated validator?
| Approach | Best fit | Tradeoff |
|---|---|---|
typedStruct | A few selected boundaries with small boolean guards | You maintain the runtime policy by hand, with compiler-checked field coverage |
| Hey API validator plugin | Generated request or response validation integrated into the SDK | Adds a validator library and follows its parsing and error model |
Choose the generated validator path when the OpenAPI document should produce runtime schemas automatically or when structured validation errors are required. Choose typedStruct when explicit, reusable predicates are the better fit for a smaller part of the application.
What typedStruct does not do
- It does not read or interpret an OpenAPI document.
- It does not generate a guard from an erased TypeScript type.
- It does not automatically attach validation to Hey API SDK calls.
- It does not coerce values or return field-level error details.
- It cannot verify that a custom predicate's implementation is honest.
Summary
- Keep generated files untouched.
- Build application-owned guards against generated object types.
- Use
optionalKeyandnullablefor their distinct OpenAPI contracts. - Compose nested generated definitions from smaller guards.
- Prefer generated validators when automatic SDK-wide validation is the real requirement.
Continue with the typedStruct API reference, the type-guard synchronization guide, or the unknown validation guide.