Working with GraphQL
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