阿里云主机折上折
  • 微信号
Current Site:Index > Working with GraphQL

Working with GraphQL

Author:Chuan Chen 阅读数:57116人阅读 分类: TypeScript

The Natural Synergy Between GraphQL and TypeScript

The strongly-typed nature of GraphQL makes it a perfect match for TypeScript. GraphQL schema itself is a type system, and TypeScript is also a type system—their combination creates a powerful development experience. When using TypeScript to write GraphQL client or server code, type safety can be maintained throughout the entire application development process.

// A typical example of combining a GraphQL query with TypeScript types
type User = {
  id: string;
  name: string;
  email: string;
  posts: Post[];
};

type Post = {
  id: string;
  title: string;
  content: string;
};

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

Type Generation Toolchain

In modern GraphQL development, type generation tools are essential. Commonly used tools like graphql-code-generator can automatically generate TypeScript type definitions from a GraphQL schema.

# Install graphql-code-generator
npm install -D @graphql-codegen/cli
npm install -D @graphql-codegen/typescript @graphql-codegen/typescript-operations

Configure the codegen.yml file:

schema: "http://localhost:4000/graphql"
documents: "./src/**/*.graphql"
generates:
  ./src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"

Server-Side Type Safety

In GraphQL server development, TypeScript can help build type-safe resolvers. Libraries like graphql and type-graphql provide seamless integration.

import { Field, ID, ObjectType, Query, Resolver } from 'type-graphql';

@ObjectType()
class User {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field()
  email: string;
}

@Resolver()
class UserResolver {
  @Query(() => [User])
  async users(): Promise<User[]> {
    // Actual database query logic
    return db.users.findMany();
  }
}

Typed Client Queries

When Apollo Client is used with TypeScript, it can automatically infer the types of query results, significantly reducing the need for manual type definitions.

import { useQuery } from '@apollo/client';
import { GetUserQuery, GetUserQueryVariables } from './generated/graphql';

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useQuery<GetUserQuery, GetUserQueryVariables>(
    GET_USER,
    {
      variables: { id: userId },
    }
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{data?.user.name}</h1>
      <p>{data?.user.email}</p>
    </div>
  );
}

Input Type Validation

Combining GraphQL input types with TypeScript allows catching many potential errors at compile time.

import { InputType, Field } from 'type-graphql';

@InputType()
class CreateUserInput {
  @Field()
  name: string;

  @Field()
  email: string;

  @Field()
  password: string;
}

@Resolver()
class UserResolver {
  @Mutation(() => User)
  async createUser(@Arg('input') input: CreateUserInput): Promise<User> {
    // Input is already type-checked
    return userService.create(input);
  }
}

Advanced Type Techniques

Leveraging TypeScript's advanced type features, you can create more flexible GraphQL type utilities.

type PickGraphQLFields<T, K extends keyof T> = {
  [P in K]: T[P];
};

// Usage example
type PartialUser = PickGraphQLFields<User, 'id' | 'name'>;

// Corresponding GraphQL fragment
const USER_FRAGMENT = gql`
  fragment UserInfo on User {
    id
    name
  }
`;

Error Handling Patterns

Combining TypeScript's union types allows for better handling of GraphQL error responses.

type GraphQLError = {
  message: string;
  locations?: { line: number; column: number }[];
  path?: string[];
};

type GraphQLResponse<T> = {
  data?: T;
  errors?: GraphQLError[];
};

async function fetchGraphQL<T, V = Record<string, unknown>>(
  query: string,
  variables?: V
): Promise<GraphQLResponse<T>> {
  const response = await fetch('/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });
  return response.json();
}

Performance Optimization

TypeScript can help identify unnecessary GraphQL field requests, optimizing network performance.

type RequiredFields<T> = {
  [K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];

function ensureFields<T>(obj: T, fields: RequiredFields<T>[]): void {
  fields.forEach((field) => {
    if (obj[field] === undefined) {
      throw new Error(`Missing required field: ${String(field)}`);
    }
  });
}

// Usage example
const user = await fetchUser();
ensureFields(user, ['id', 'name']); // Ensure these fields are requested

Custom Scalar Types

GraphQL's custom scalar types can perfectly map to TypeScript types.

import { GraphQLScalarType, Kind } from 'graphql';
import { Scalar } from 'type-graphql';

@Scalar('Date')
class DateScalar {
  description = 'Date custom scalar type';

  parseValue(value: string): Date {
    return new Date(value); // From client input
  }

  serialize(value: Date): string {
    return value.toISOString(); // Sent to client
  }

  parseLiteral(ast: any): Date | null {
    if (ast.kind === Kind.STRING) {
      return new Date(ast.value);
    }
    return null;
  }
}

Type-Safe Subscriptions

GraphQL subscriptions can also benefit from TypeScript's type safety.

import { PubSub } from 'graphql-subscriptions';
import { Subscription, Root, Args } from 'type-graphql';

const pubSub = new PubSub();

@Resolver()
class MessageResolver {
  @Subscription(() => Message, {
    topics: 'MESSAGE_ADDED',
    filter: ({ payload, args }) => payload.channelId === args.channelId,
  })
  messageAdded(
    @Root() message: Message,
    @Args() { channelId }: { channelId: string }
  ): Message {
    return message;
  }
}

Integration with Utility Libraries

Popular GraphQL utility libraries like urql and react-query also offer excellent TypeScript support.

import { useQuery } from 'urql';
import { graphql } from './gql';

const GetUserDocument = graphql(`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
    }
  }
`);

function UserComponent({ id }: { id: string }) {
  const [result] = useQuery({
    query: GetUserDocument,
    variables: { id },
  });

  // result.data.user already has the correct type
}

Type Safety in Testing

TypeScript can also assist in testing GraphQL APIs.

import { createTestClient } from 'apollo-server-testing';
import { server } from './server';
import { gql } from 'apollo-server';

const { query } = createTestClient(server);

test('fetch user', async () => {
  const GET_USER = gql`
    query GetUser($id: ID!) {
      user(id: $id) {
        id
        name
      }
    }
  `;

  const result = await query({
    query: GET_USER,
    variables: { id: '1' },
  });

  // TypeScript knows the possible structure of result.data
  expect(result.data?.user?.name).toBeDefined();
});

Sharing Types Between Frontend and Backend

Through code generation, type definitions can be shared between frontend and backend.

// Shared type definitions
export type User = {
  id: string;
  name: string;
  email: string;
};

// Backend resolver
@Resolver()
class UserResolver {
  @Query(() => [User])
  async users(): Promise<User[]> {
    // ...
  }
}

// Frontend component
function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Handling Complex Union Types

GraphQL's union types and interfaces can map well to TypeScript's union types.

type SearchResult = Book | Author;

interface Book {
  __typename: 'Book';
  title: string;
  author: string;
}

interface Author {
  __typename: 'Author';
  name: string;
  books: string[];
}

function renderSearchResult(result: SearchResult) {
  switch (result.__typename) {
    case 'Book':
      return <BookComponent book={result} />;
    case 'Author':
      return <AuthorComponent author={result} />;
    default:
      // TypeScript ensures all cases are handled
      const _exhaustiveCheck: never = result;
      return _exhaustiveCheck;
  }
}

Type-Safe GraphQL Directives

TypeScript can help validate the use of GraphQL directives.

import { Directive, DirectiveLocation, GraphQLDirective } from 'graphql';

const deprecatedDirective = new GraphQLDirective({
  name: 'deprecated',
  locations: [
    DirectiveLocation.FIELD_DEFINITION,
    DirectiveLocation.ENUM_VALUE,
  ],
  args: {
    reason: {
      type: GraphQLString,
      defaultValue: 'No longer supported',
    },
  },
});

// TypeScript types can ensure directive arguments are correct
type DirectiveArgs = {
  reason?: string;
};

function applyDirective(field: string, args: DirectiveArgs) {
  // Implement directive logic
}

Performance Monitoring with Types

Combining TypeScript allows for typed performance monitoring tools.

type GraphQLMetrics = {
  operationName: string;
  executionTime: number;
  fieldResolveTimes: Record<string, number>;
  errors?: GraphQLError[];
};

function trackPerformance<T>(
  operation: string,
  fn: () => Promise<T>
): Promise<{ result: T; metrics: GraphQLMetrics }> {
  const start = performance.now();
  const fieldTimes: Record<string, number> = {};

  return fn().then((result) => ({
    result,
    metrics: {
      operationName: operation,
      executionTime: performance.now() - start,
      fieldResolveTimes: fieldTimes,
    },
  }));
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:与Express/Koa框架

下一篇:与Web Workers

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.