Кодогенерация и типизация GraphQL
GraphQL-схема — контракт между сервером и клиентом. GraphQL Code Generator автоматически создаёт TypeScript-типы из схемы и операций, исключая ручное поддержание синхронности типов и runtime-ошибки несоответствия данных.
Установка GraphQL Code Generator
npm install -D @graphql-codegen/cli @graphql-codegen/typescript \
@graphql-codegen/typescript-resolvers \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo \
@graphql-codegen/introspection
Конфигурация codegen.yml
# codegen.yml
overwrite: true
schema: "http://localhost:4000/graphql" # или путь к SDL файлу
documents: "src/**/*.graphql" # клиентские операции
generates:
# Серверные типы (резолверы)
src/generated/graphql-server.ts:
plugins:
- typescript
- typescript-resolvers
config:
contextType: "../context#GraphQLContext"
mappers:
# Маппинг GraphQL-типов на реальные модели БД
User: "../models/User#UserModel"
Post: "../models/Post#PostModel"
useIndexSignature: true
enumsAsTypes: true
avoidOptionals:
field: true
# Клиентские типы (операции + хуки)
src/generated/graphql-client.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
withComponent: false
withHOC: false
dedupeFragments: true
# Introspection для IDE
src/generated/introspection.json:
plugins:
- introspection
Серверные типы резолверов
После генерации типы Resolvers точно соответствуют схеме:
// src/generated/graphql-server.ts (фрагмент)
export type QueryResolvers<ContextType = GraphQLContext> = {
user?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<QueryUserArgs, 'id'>>;
posts?: Resolver<ResolversTypes['PostConnection'], ParentType, ContextType, Partial<QueryPostsArgs>>;
}
export type UserResolvers<ContextType = GraphQLContext> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
posts?: Resolver<Array<ResolversTypes['Post']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}
// src/resolvers/user.resolver.ts
import { QueryResolvers, UserResolvers } from '../generated/graphql-server'
import { GraphQLContext } from '../context'
export const userQueryResolvers: QueryResolvers = {
user: async (parent, { id }, ctx: GraphQLContext) => {
// TypeScript знает: id: string, возврат: UserModel | null
return ctx.db.users.findById(id)
}
}
export const userTypeResolvers: UserResolvers = {
posts: async (parent, args, ctx) => {
// parent типизирован как UserModel (из mappers)
return ctx.loaders.postsByUserId.load(parent.id)
}
}
// Собранные резолверы с полной типизацией
export const resolvers = {
Query: userQueryResolvers,
User: userTypeResolvers,
}
Клиентские операции с хуками
# src/operations/posts.graphql
query GetPosts($limit: Int, $after: String) {
posts(first: $limit, after: $after) {
edges {
cursor
node {
id
title
author {
id
name
}
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
slug
}
}
// После codegen доступны типизированные хуки:
import { useGetPostsQuery, useCreatePostMutation } from './generated/graphql-client'
function PostList() {
const { data, loading, fetchMore } = useGetPostsQuery({
variables: { limit: 20 }
})
// data.posts.edges — полностью типизировано
// TypeScript выведет ошибку при обращении к несуществующим полям
const [createPost] = useCreatePostMutation({
onCompleted: (data) => {
// data.createPost.id — типизировано
console.log('Created:', data.createPost.id)
}
})
return (
<div>
{data?.posts.edges.map(({ node, cursor }) => (
<PostCard key={cursor} post={node} />
))}
</div>
)
}
Фрагменты и Fragment Masking
# src/operations/fragments.graphql
fragment UserAvatar on User {
id
name
avatarUrl
}
fragment PostCard on Post {
id
title
excerpt
author {
...UserAvatar
}
}
// Fragment Masking: компонент получает только те данные, которые запросил
import { FragmentType, useFragment } from './generated/fragment-masking'
import { PostCardFragmentDoc } from './generated/graphql-client'
function PostCard({ post }: { post: FragmentType<typeof PostCardFragmentDoc> }) {
const data = useFragment(PostCardFragmentDoc, post)
// data типизирован как PostCardFragment — только запрошенные поля
}
Автоматизация в CI/CD
// package.json
{
"scripts": {
"codegen": "graphql-codegen --config codegen.yml",
"codegen:watch": "graphql-codegen --config codegen.yml --watch",
"type-check": "tsc --noEmit",
"lint": "eslint src --ext .ts,.tsx"
}
}
# .github/workflows/codegen-check.yml
name: GraphQL Codegen Check
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- name: Start GraphQL server
run: npm run dev &
- name: Wait for server
run: npx wait-on http://localhost:4000/graphql
- name: Run codegen
run: npm run codegen
- name: Check for uncommitted changes
run: |
if [[ -n $(git diff --name-only) ]]; then
echo "Generated files are out of sync. Run npm run codegen."
git diff
exit 1
fi
- name: TypeScript check
run: npm run type-check
Серверные валидационные утилиты
// Кодоген создаёт типы входных данных для мутаций
import { CreatePostInput, MutationCreatePostArgs } from './generated/graphql-server'
import { z } from 'zod'
// Zod-схема на базе сгенерированного типа
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
tags: z.array(z.string()).max(10).optional(),
} satisfies Record<keyof CreatePostInput, z.ZodTypeAny>)
// TypeScript проверит, что схема покрывает все поля типа
Срок выполнения
Настройка GraphQL Code Generator для серверных резолверов и клиентских хуков с CI-проверкой синхронности — 1–2 рабочих дня.







