Грани типизации: Как применять строгие TypeScript типы в GraphQL резолверах

text
graphql
scalar Date

type User {
  id: ID!
  name: String!
  email: String!
  createdAt: Date!
}

type Query {
  getUser(id: ID!): User
  listUsers(limit: Int = 10): [User!]!
}

input CreateUserInput {
  name: String!
  email: String!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
}
typescript
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: './src/schema.graphql',
  generates: {
    './src/generated/graphql.ts': {
      plugins: [
        'typescript',
        'typescript-resolvers',
        'typescript-document-nodes'
      ],
      config: {
        useIndexSignature: true,
        contextType: '../context#Context',
        mapperTypeSuffix: 'Model',
        mappers: {
          User: '../models#UserModel'
        },
        scalars: {
          Date: 'Date'
        }
      }
    }
  }
};

export default config;
typescript
// src/models/user.ts
export interface UserModel {
  id: string;
  name: string;
  email: string;
  created_at: Date;  // Разница в именовании между БД и API
}

// src/context.ts
import type { UserModel } from './models/user';

export interface Context {
  userLoader: DataLoader<string, UserModel>;
  // Другие инструменты контекста
}
typescript
// Пример типизированного резолвера
const userResolver: QueryResolvers<Context>['getUser'] = async (
  _parent,
  { id },
  context,
  _info
) => {
  /*
   * Теперь резолвер знает:
   * - args автоматически типизированы как { id: string }
   * - context соответствует нашему интерфейсу Context
   * - Возвращаемое значение должно соответствовать User | null | undefined
   */
  return context.userLoader.load(id);
};

const createUserResolver: MutationResolvers<Context>['createUser'] = async (
  _parent,
  { input },
  context
) => {
  // Система знает структуру input благодаря CreateUserInput
  if (input.email.includes('spam')) {
    throw new UserInputError('Invalid email detected');
  }
  
  const newUser = await userService.createUser({
    name: input.name,
    email: input.email
  });

  // Автоматическая проверка соответствия типа UserModel
  return newUser;
};

При работе с контекстом укажите:

typescript
import { UserModel } from '../models/user';
import { DataLoader } from '../utils/data-loader';

type Loaders = {
  userLoader: DataLoader<string, UserModel>;
};

export type Context = {
  loaders: Loaders;
  userId?: string;
};

// В Apollo Server
const server = new ApolloServer<Context>({
  typeDefs,
  resolvers,
  context: ({ req }) => ({
    userId: getUserIdFromRequest(req),
    loaders: {
      userLoader: createUserLoader()
    }
  })
});

Для оптимизации производительности используйте DataLoader:

typescript
type UserModel = {
  userId: string;
  name: string;
};

export const createUserLoader = () => {
  return new DataLoader<string, UserModel>(async (userIds) => {
    const users = await db.user.findMany({
      where: { userId: { in: [...userIds] } },
    });
    
    return userIds.map((id) => 
      users.find((u) => u.userId === id) || new NotFoundError(`User ${id}`)
    );
  });
};

Для прохождения полного цикла разработки:

bash
npm start    # Запускает сервер разработки с пересборкой при изменениях
npm run codegen  # Регенерирует типы при изменении схемы

В заключение: чем вы выигрываете

  1. Типизированные резолверы: Каждый резолвер знает точную форму своих параметров и ожидаемый тип результата. Ошибки несоответствия типов обнаруживаются при компиляции, а не в рантайме.

  2. Согласованность контракта: Изменения в схеме немедленно отражаются в TypeScript типах, вынуждая разработчиков корректировать реализацию в соответствии с новыми требованиями.

  3. Контекстная безопасность: Типизированный контекст исключает ошибки доступа к несуществующим свойствам и обеспечивает доступность инструментов там, где они реально нужны.

  4. Совместимость с ORM: Благодаря гибкой системе маппингов вы поддерживаете связь между отличными моделями GraphQL и объектами предметной области.

Для команд, требовательных к качеству кода, сочетание GraphQL Code Generator и TypeScript – это не просто приятная опция, а необходимый инструмент для создания предсказуемых, легко поддерживаемых бэкенд-систем. При правильной настройке он окупает вложенные усилия многократным сокращением ошибок типа null-pointer exceptions или неожиданных undefined значений.