لغة TypeScript

TypeScript مع GraphQL

25 دقيقة الدرس 36 من 40

TypeScript مع GraphQL

GraphQL وTypeScript يشكلان تطابقاً مثالياً. نهج GraphQL القائم على المخطط يوفر أماناً تلقائياً للأنواع، بينما يضمن TypeScript أن استعلاماتك ومحللاتك آمنة من حيث النوع. في هذا الدرس، سنستكشف الاستعلامات المكتوبة، توليد الكود، المحللات المكتوبة، وأنماط تكامل GraphQL العملية.

لماذا GraphQL + TypeScript؟

GraphQL وTypeScript يكملان بعضهما البعض بشكل مثالي:

  • أنواع مدفوعة بالمخطط: مخططات GraphQL تولد أنواع TypeScript تلقائياً
  • أمان من البداية للنهاية: الأنواع تتدفق من المخطط إلى المحللات إلى استعلامات العميل
  • الإكمال التلقائي: بيئات التطوير توفر اقتراحات استعلامات GraphQL بناءً على المخطط
  • ثقة في إعادة الهيكلة: تغييرات المخطط تظهر أخطاء الأنواع فوراً
  • التوثيق: الأنواع تعمل كتوثيق حي

أساسيات GraphQL

أولاً، لنفهم أساسيات GraphQL:

# GraphQL Schema (schema.graphql) type User { id: ID! name: String! email: String! posts: [Post!]! } type Post { id: ID! title: String! content: String! author: User! publishedAt: DateTime } type Query { user(id: ID!): User users: [User!]! post(id: ID!): Post } type Mutation { createPost(input: CreatePostInput!): Post! updatePost(id: ID!, input: UpdatePostInput!): Post! } input CreatePostInput { title: String! content: String! authorId: ID! } input UpdatePostInput { title: String content: String }

توليد كود GraphQL

استخدم graphql-codegen لتوليد أنواع TypeScript من مخططك:

// تثبيت التبعيات npm install --save-dev @graphql-codegen/cli \ @graphql-codegen/typescript \ @graphql-codegen/typescript-operations \ @graphql-codegen/typescript-resolvers
// codegen.yml schema: "./schema.graphql" documents: "./src/**/*.graphql" generates: ./src/generated/types.ts: plugins: - typescript - typescript-operations - typescript-resolvers config: useIndexSignature: true enumsAsTypes: true avoidOptionals: false maybeValue: T | null

هذا يولد أنواع TypeScript شاملة:

// generated/types.ts (مقتطف) export type User = { __typename?: 'User'; id: Scalars['ID']; name: Scalars['String']; email: Scalars['String']; posts: Array<Post>; }; export type Post = { __typename?: 'Post'; id: Scalars['ID']; title: Scalars['String']; content: Scalars['String']; author: User; publishedAt?: Maybe<Scalars['DateTime']>; }; export type QueryUserArgs = { id: Scalars['ID']; }; export type MutationCreatePostArgs = { input: CreatePostInput; };

محللات GraphQL المكتوبة

أنشئ محللات آمنة من حيث النوع باستخدام الأنواع المولدة:

import { Resolvers, User, Post } from './generated/types'; // نماذج قاعدة البيانات (مثال) interface UserModel { id: string; name: string; email: string; } interface PostModel { id: string; title: string; content: string; authorId: string; publishedAt: Date | null; } // نوع السياق interface Context { db: { users: UserModel[]; posts: PostModel[]; }; userId?: string; } // محللات مكتوبة const resolvers: Resolvers<Context> = { Query: { user: async (_, { id }, context) => { const user = context.db.users.find(u => u.id === id); if (!user) return null; return user; }, users: async (_, __, context) => { return context.db.users; }, post: async (_, { id }, context) => { const post = context.db.posts.find(p => p.id === id); if (!post) return null; return post; } }, Mutation: { createPost: async (_, { input }, context) => { if (!context.userId) { throw new Error('Not authenticated'); } const post: PostModel = { id: generateId(), title: input.title, content: input.content, authorId: input.authorId, publishedAt: new Date() }; context.db.posts.push(post); return post; } }, User: { posts: async (user, _, context) => { return context.db.posts.filter(p => p.authorId === user.id); } }, Post: { author: async (post, _, context) => { const author = context.db.users.find(u => u.id === post.authorId); if (!author) throw new Error('Author not found'); return author; } } };
ملاحظة: نوع Resolvers المولد يضمن أن جميع المحللات تطابق المخطط، مع أنواع معاملات صحيحة، أنواع إرجاع، وسياق.

عميل GraphQL آمن من حيث النوع (graphql-request)

استخدم graphql-request للاستعلامات المكتوبة للعميل:

npm install graphql graphql-request
import { GraphQLClient, gql } from 'graphql-request'; import { User, Post, CreatePostInput } from './generated/types'; const client = new GraphQLClient('http://localhost:4000/graphql', { headers: { authorization: 'Bearer token' } }); // استعلام مكتوب async function getUser(id: string): Promise<User | null> { const query = gql` query GetUser($id: ID!) { user(id: $id) { id name email posts { id title publishedAt } } } `; const data = await client.request<{ user: User | null }>(query, { id }); return data.user; } // طفرة مكتوبة async function createPost(input: CreatePostInput): Promise<Post> { const mutation = gql` mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id title content author { id name } publishedAt } } `; const data = await client.request<{ createPost: Post }>(mutation, { input }); return data.createPost; } // الاستخدام async function demo() { const user = await getUser('123'); if (user) { console.log(`User: ${user.name}`); console.log(`Posts: ${user.posts.length}`); } const newPost = await createPost({ title: 'TypeScript + GraphQL', content: 'Amazing combination!', authorId: '123' }); console.log(`Created post: ${newPost.title}`); }

متقدم: عمليات GraphQL المكتوبة

ولد أنواعاً لعمليات محددة باستخدام graphql-codegen:

// queries.graphql query GetUserWithPosts($id: ID!) { user(id: $id) { id name email posts { id title content publishedAt } } } mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id title content author { id name } } }
// codegen.yml (محسّن) generates: ./src/generated/types.ts: plugins: - typescript - typescript-operations config: namingConvention: typeNames: pascal-case enumValues: upper-case

هذا يولد أنواعاً خاصة بالعمليات:

// generated/types.ts (إضافي) export type GetUserWithPostsQuery = { __typename?: 'Query'; user?: { __typename?: 'User'; id: string; name: string; email: string; posts: Array<{ __typename?: 'Post'; id: string; title: string; content: string; publishedAt?: string | null; }>; } | null; }; export type GetUserWithPostsQueryVariables = { id: string; }; export type CreatePostMutation = { __typename?: 'Mutation'; createPost: { __typename?: 'Post'; id: string; title: string; content: string; author: { __typename?: 'User'; id: string; name: string; }; }; }; export type CreatePostMutationVariables = { input: CreatePostInput; };
// الاستخدام مع أنواع العمليات import { GetUserWithPostsQuery, GetUserWithPostsQueryVariables } from './generated/types'; async function getUserWithPosts(id: string): Promise<GetUserWithPostsQuery['user']> { const query = gql` query GetUserWithPosts($id: ID!) { user(id: $id) { id name email posts { id title content publishedAt } } } `; const variables: GetUserWithPostsQueryVariables = { id }; const data = await client.request<GetUserWithPostsQuery>(query, variables); return data.user; }

TypeScript + Apollo Server

استخدم Apollo Server مع TypeScript لواجهة GraphQL API آمنة من حيث النوع بالكامل:

npm install apollo-server graphql npm install --save-dev @graphql-tools/schema
import { ApolloServer } from 'apollo-server'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { readFileSync } from 'fs'; import { Resolvers } from './generated/types'; // تحميل المخطط const typeDefs = readFileSync('./schema.graphql', 'utf-8'); // محللات آمنة من حيث النوع const resolvers: Resolvers = { Query: { user: async (_, { id }, context) => { return context.dataSources.users.findById(id); } } }; // إنشاء المخطط const schema = makeExecutableSchema({ typeDefs, resolvers }); // نوع السياق interface Context { dataSources: { users: UserDataSource; posts: PostDataSource; }; userId?: string; } // إنشاء الخادم const server = new ApolloServer({ schema, context: ({ req }): Context => ({ dataSources: { users: new UserDataSource(), posts: new PostDataSource() }, userId: extractUserId(req.headers.authorization) }) }); server.listen().then(({ url }) => { console.log(`Server ready at ${url}`); });

معالجة الأخطاء

import { GraphQLError } from 'graphql'; const resolvers: Resolvers = { Query: { user: async (_, { id }, context) => { const user = await context.dataSources.users.findById(id); if (!user) { throw new GraphQLError('User not found', { extensions: { code: 'USER_NOT_FOUND', userId: id } }); } return user; } }, Mutation: { createPost: async (_, { input }, context) => { if (!context.userId) { throw new GraphQLError('Authentication required', { extensions: { code: 'UNAUTHENTICATED' } }); } try { return await context.dataSources.posts.create(input); } catch (error) { throw new GraphQLError('Failed to create post', { extensions: { code: 'CREATE_POST_FAILED', originalError: error } }); } } } };
نصيحة: استخدم امتدادات أخطاء GraphQL لتوفير معلومات أخطاء منظمة مع كتابة صحيحة.

دعم الاشتراكات

// schema.graphql type Subscription { postCreated: Post! userUpdated(userId: ID!): User! }
import { PubSub } from 'graphql-subscriptions'; const pubsub = new PubSub(); const resolvers: Resolvers = { Mutation: { createPost: async (_, { input }, context) => { const post = await context.dataSources.posts.create(input); // نشر حدث الاشتراك pubsub.publish('POST_CREATED', { postCreated: post }); return post; } }, Subscription: { postCreated: { subscribe: () => pubsub.asyncIterator(['POST_CREATED']) }, userUpdated: { subscribe: (_, { userId }) => { return pubsub.asyncIterator([`USER_UPDATED_${userId}`]); } } } };
تمرين: أنشئ GraphQL API باستخدام TypeScript:
  1. عرف مخططاً لمدونة (منشورات، تعليقات، مؤلفون)
  2. ولد أنواع TypeScript باستخدام graphql-codegen
  3. نفذ محللات مكتوبة مع Apollo Server
  4. أنشئ عميلاً مكتوباً باستخدام graphql-request
  5. أضف دعم الاشتراكات للتحديثات في الوقت الفعلي
  6. نفذ معالجة مناسبة للأخطاء مع رموز أخطاء مخصصة

الخلاصة

في هذا الدرس، تعلمت كيفية دمج TypeScript مع GraphQL لأمان الأنواع من البداية للنهاية. غطينا توليد الأنواع بناءً على المخطط، المحللات المكتوبة، استعلامات العميل الآمنة من حيث النوع، ومعالجة الأخطاء. هذا المزيج يوفر تجربة مطور لا مثيل لها وثقة في واجهات GraphQL APIs الخاصة بك.