لغة TypeScript

TypeScript مع Node.js

28 دقيقة الدرس 20 من 40

TypeScript مع Node.js

يجلب TypeScript فوائد كبيرة لتطوير Node.js، بما في ذلك اكتشاف أفضل للأخطاء، ودعم محسّن لبيئة التطوير المتكاملة، وكود أكثر قابلية للصيانة. في هذا الدرس، سنستكشف كيفية بناء تطبيقات Node.js آمنة من حيث النوع مع Express، تغطي كل شيء من الإعداد الأساسي إلى أنماط الكتابة المتقدمة.

إعداد مشروع TypeScript Node.js

ابدأ بتهيئة مشروع Node.js مع تكوين TypeScript والتبعيات اللازمة.

إعداد المشروع:
<# تهيئة المشروع
mkdir my-node-app
cd my-node-app
npm init -y

# تثبيت TypeScript وأنواع Node
npm install --save-dev typescript @types/node

# تهيئة تكوين TypeScript
npx tsc --init

# تثبيت تبعيات إضافية
npm install express
npm install --save-dev @types/express

# تثبيت أدوات التطوير
npm install --save-dev ts-node nodemon

# هيكل المشروع
my-node-app/
├── src/
│   ├── index.ts
│   ├── routes/
│   ├── controllers/
│   ├── middleware/
│   └── types/
├── dist/           # JavaScript المترجم
├── tsconfig.json
└── package.json
>

تكوين TypeScript لـ Node.js

قم بتكوين tsconfig.json بإعدادات محسّنة لتطوير Node.js.

tsconfig.json:
<{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "moduleResolution": "node",
    "types": ["node"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
>
سكريبتات package.json:
<{
  "scripts": {
    "dev": "nodemon --exec ts-node src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "type-check": "tsc --noEmit"
  }
}
>

كتابة تطبيق Express

أنشئ تطبيق Express آمن من حيث النوع مع مسارات ووسائط ومعالجات مكتوبة بشكل صحيح.

إعداد Express الأساسي:
<// src/index.ts
import express, { Application } from 'express';

const app: Application = express();
const PORT = process.env.PORT || 3000;

// الوسائط
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// المسارات
app.get('/', (req, res) => {
  res.json({ message: 'مرحبًا، TypeScript!' });
});

app.listen(PORT, () => {
  console.log(`الخادم يعمل على المنفذ ${PORT}`);
});
>

كتابة Request و Response

يوفر Express أنواعًا عامة لكائنات Request و Response، مما يسمح لك بتحديد أنواع مخصصة للمعاملات والاستعلام والجسم وبيانات الاستجابة.

كتابة Request/Response:
<import { Request, Response, NextFunction } from 'express';

// كتابة معاملات المسار
interface UserParams {
  id: string;
}

// كتابة معاملات الاستعلام
interface UserQuery {
  includeProfile?: string;
  fields?: string;
}

// كتابة جسم الطلب
interface CreateUserBody {
  name: string;
  email: string;
  password: string;
}

// كتابة جسم الاستجابة
interface UserResponse {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// معالج مسار مكتوب بالكامل
app.get(
  '/users/:id',
  (
    req: Request<UserParams, {}, {}, UserQuery>,
    res: Response<UserResponse>
  ) => {
    const userId = req.params.id; // النوع: string
    const includeProfile = req.query.includeProfile; // النوع: string | undefined

    // TypeScript يفرض نوع الاستجابة
    res.json({
      id: parseInt(userId),
      name: 'جون دو',
      email: 'john@example.com',
      createdAt: new Date()
    });
  }
);

// مسار POST مع كتابة الجسم
app.post(
  '/users',
  (
    req: Request<{}, UserResponse, CreateUserBody>,
    res: Response<UserResponse>
  ) => {
    const { name, email, password } = req.body;

    // TypeScript يعرف الأنواع
    console.log(name.toUpperCase()); // آمن من حيث النوع
    // console.log(req.body.invalid); // خطأ: الخاصية غير موجودة

    res.status(201).json({
      id: 1,
      name,
      email,
      createdAt: new Date()
    });
  }
);
>
ملاحظة: توقيع Request العام: Request<Params, ResBody, ReqBody, ReqQuery>

كتابة دوال الوسائط

يمكن كتابة دوال الوسائط في Express لاكتشاف أفضل للأخطاء ودعم الإكمال التلقائي.

كتابة الوسائط:
<import { Request, Response, NextFunction, RequestHandler } from 'express';

// نوع وسيط أساسي
const logger = (req: Request, res: Response, next: NextFunction): void => {
  console.log(`${req.method} ${req.path}`);
  next();
};

// استخدام نوع RequestHandler
const authenticate: RequestHandler = (req, res, next) => {
  const token = req.headers.authorization;

  if (!token) {
    return res.status(401).json({ error: 'غير مصرح' });
  }

  // منطق التحقق من الرمز
  next();
};

// وسيط مع خصائص طلب مخصصة
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string;
        email: string;
        role: 'admin' | 'user';
      };
    }
  }
}

const attachUser: RequestHandler = (req, res, next) => {
  // إرفاق المستخدم بالطلب
  req.user = {
    id: '123',
    email: 'user@example.com',
    role: 'user'
  };
  next();
};

// استخدام الوسيط
app.use(logger);
app.use(authenticate);
app.use(attachUser);

// الوصول إلى الخصائص المخصصة في المسارات
app.get('/profile', (req, res) => {
  // TypeScript يعرف عن req.user
  if (req.user) {
    res.json({ user: req.user });
  } else {
    res.status(401).json({ error: 'غير مصادق عليه' });
  }
});
>
نصيحة: استخدم دمج الإعلانات لتوسيع أنواع Express بخصائص مخصصة دون تعديل تعريفات الأنواع الأصلية.

كتابة معالجات المسارات غير المتزامنة

تتطلب معالجات المسارات غير المتزامنة معالجة أخطاء مناسبة. أنشئ غلافًا للتعامل مع الأخطاء غير المتزامنة تلقائيًا.

غلاف المعالج غير المتزامن:
<import { Request, Response, NextFunction, RequestHandler } from 'express';

// غلاف غير متزامن آمن من حيث النوع
type AsyncRequestHandler<
  P = any,
  ResBody = any,
  ReqBody = any,
  ReqQuery = any
> = (
  req: Request<P, ResBody, ReqBody, ReqQuery>,
  res: Response<ResBody>,
  next: NextFunction
) => Promise<any>;

const asyncHandler = <P, ResBody, ReqBody, ReqQuery>(
  fn: AsyncRequestHandler<P, ResBody, ReqBody, ReqQuery>
): RequestHandler<P, ResBody, ReqBody, ReqQuery> => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

// الاستخدام مع معالج غير متزامن مكتوب
interface GetUserParams {
  id: string;
}

interface UserResponse {
  id: number;
  name: string;
  email: string;
}

app.get(
  '/users/:id',
  asyncHandler<GetUserParams, UserResponse, {}, {}>(
    async (req, res) => {
      const userId = parseInt(req.params.id);

      // محاكاة استدعاء قاعدة البيانات
      const user = await fetchUserFromDB(userId);

      if (!user) {
        res.status(404).json({ error: 'المستخدم غير موجود' } as any);
        return;
      }

      // استجابة آمنة من حيث النوع
      res.json({
        id: user.id,
        name: user.name,
        email: user.email
      });
    }
  )
);

// دالة غير متزامنة محاكاة
async function fetchUserFromDB(id: number): Promise<UserResponse | null> {
  // منطق قاعدة البيانات هنا
  return {
    id,
    name: 'جون دو',
    email: 'john@example.com'
  };
}
>

كتابة متغيرات البيئة

متغيرات البيئة غير مكتوبة بشكل افتراضي. أنشئ تكوين بيئة آمن من حيث النوع.

متغيرات البيئة:
<// src/types/environment.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: 'development' | 'production' | 'test';
      PORT: string;
      DATABASE_URL: string;
      JWT_SECRET: string;
      API_KEY: string;
    }
  }
}

export {};

// src/config/env.ts
interface Config {
  env: string;
  port: number;
  database: {
    url: string;
  };
  jwt: {
    secret: string;
  };
  api: {
    key: string;
  };
}

const getConfig = (): Config => {
  // التحقق من صحة متغيرات البيئة المطلوبة
  const requiredEnvVars = [
    'NODE_ENV',
    'PORT',
    'DATABASE_URL',
    'JWT_SECRET',
    'API_KEY'
  ];

  for (const envVar of requiredEnvVars) {
    if (!process.env[envVar]) {
      throw new Error(`متغير البيئة المطلوب مفقود: ${envVar}`);
    }
  }

  return {
    env: process.env.NODE_ENV,
    port: parseInt(process.env.PORT, 10),
    database: {
      url: process.env.DATABASE_URL
    },
    jwt: {
      secret: process.env.JWT_SECRET
    },
    api: {
      key: process.env.API_KEY
    }
  };
};

export const config = getConfig();

// الاستخدام - آمن تمامًا من حيث النوع
import { config } from './config/env';

console.log(config.port); // النوع: number
console.log(config.env); // النوع: string
console.log(config.database.url); // النوع: string
>
تحذير: تحقق دائمًا من صحة متغيرات البيئة عند بدء التشغيل. يمكن أن تتسبب متغيرات البيئة المفقودة أو غير الصالحة في أخطاء وقت التشغيل.

كتابة نماذج قاعدة البيانات

أنشئ نماذج واستعلامات قاعدة بيانات آمنة من حيث النوع لجودة كود أفضل.

أنواع نماذج قاعدة البيانات:
<// src/types/models.ts
export interface User {
  id: number;
  email: string;
  password: string;
  name: string;
  role: 'admin' | 'user';
  createdAt: Date;
  updatedAt: Date;
}

export interface Post {
  id: number;
  title: string;
  content: string;
  authorId: number;
  published: boolean;
  createdAt: Date;
  updatedAt: Date;
}

// أنواع DTO (كائنات نقل البيانات)
export type CreateUserDTO = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
export type UpdateUserDTO = Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>;
export type UserResponse = Omit<User, 'password'>;

// src/services/userService.ts
import { User, CreateUserDTO, UpdateUserDTO, UserResponse } from '../types/models';

class UserService {
  async create(data: CreateUserDTO): Promise<UserResponse> {
    // منطق إدراج قاعدة البيانات
    const user: User = {
      id: 1,
      ...data,
      createdAt: new Date(),
      updatedAt: new Date()
    };

    // حذف كلمة المرور قبل الإرجاع
    const { password, ...userResponse } = user;
    return userResponse;
  }

  async findById(id: number): Promise<UserResponse | null> {
    // منطق استعلام قاعدة البيانات
    return null;
  }

  async update(id: number, data: UpdateUserDTO): Promise<UserResponse> {
    // منطق تحديث قاعدة البيانات
    return {} as UserResponse;
  }

  async delete(id: number): Promise<void> {
    // منطق حذف قاعدة البيانات
  }
}

export const userService = new UserService();
>

معالجة الأخطاء بأنواع مخصصة

أنشئ معالجة أخطاء آمنة من حيث النوع مع فئات أخطاء مخصصة ووسيط الأخطاء.

أنواع الأخطاء المخصصة:
<// src/types/errors.ts
export class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public isOperational: boolean = true
  ) {
    super(message);
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

export class ValidationError extends AppError {
  constructor(message: string) {
    super(400, message);
  }
}

export class NotFoundError extends AppError {
  constructor(message: string = 'المورد غير موجود') {
    super(404, message);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message: string = 'غير مصرح') {
    super(401, message);
  }
}

// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../types/errors';

interface ErrorResponse {
  status: 'error';
  statusCode: number;
  message: string;
  stack?: string;
}

export const errorHandler = (
  err: Error,
  req: Request,
  res: Response<ErrorResponse>,
  next: NextFunction
): void => {
  if (err instanceof AppError) {
    res.status(err.statusCode).json({
      status: 'error',
      statusCode: err.statusCode,
      message: err.message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    });
    return;
  }

  // أخطاء غير متوقعة
  console.error('خطأ غير متوقع:', err);
  res.status(500).json({
    status: 'error',
    statusCode: 500,
    message: 'خطأ داخلي في الخادم',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
};

// الاستخدام في المسارات
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await userService.findById(parseInt(req.params.id));

  if (!user) {
    throw new NotFoundError('المستخدم غير موجود');
  }

  res.json(user);
}));

// تسجيل معالج الأخطاء (يجب أن يكون الأخير)
app.use(errorHandler);
>

كتابة استجابات API

أنشئ هياكل استجابة API متسقة وآمنة من حيث النوع.

أنواع الاستجابة:
<// src/types/responses.ts
export interface ApiResponse<T = any> {
  success: boolean;
  data?: T;
  error?: {
    message: string;
    code?: string;
  };
  meta?: {
    page?: number;
    limit?: number;
    total?: number;
  };
}

// دوال مساعدة
export const successResponse = <T>(data: T, meta?: ApiResponse['meta']): ApiResponse<T> => ({
  success: true,
  data,
  ...(meta && { meta })
});

export const errorResponse = (message: string, code?: string): ApiResponse => ({
  success: false,
  error: { message, code }
});

// الاستخدام في المتحكمات
import { successResponse, errorResponse } from '../types/responses';

app.get('/users', asyncHandler(async (req, res) => {
  const users = await userService.findAll();

  res.json(successResponse(users, {
    page: 1,
    limit: 10,
    total: users.length
  }));
}));

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await userService.findById(parseInt(req.params.id));

  if (!user) {
    return res.status(404).json(errorResponse('المستخدم غير موجود', 'USER_NOT_FOUND'));
  }

  res.json(successResponse(user));
}));
>
تمرين: أنشئ Express API مكتوب بالكامل يحتوي على:
  1. مسارات مكتوبة بشكل صحيح مع المعاملات والاستعلام والجسم
  2. يستخدم وسيطًا مخصصًا لإرفاق معلومات المستخدم بالطلبات
  3. ينفذ معالجة أخطاء غير متزامنة مع فئات أخطاء مخصصة
  4. لديه تكوين متغير بيئة آمن من حيث النوع
  5. يُرجع استجابات API متسقة ومكتوبة

مثال كامل: Express API مكتوب

التطبيق الكامل:
<// src/index.ts
import express from 'express';
import { config } from './config/env';
import { errorHandler } from './middleware/errorHandler';
import { authenticate } from './middleware/auth';
import userRoutes from './routes/userRoutes';

const app = express();

// الوسائط
app.use(express.json());
app.use(authenticate);

// المسارات
app.use('/api/users', userRoutes);

// فحص الصحة
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date() });
});

// معالجة الأخطاء
app.use(errorHandler);

// بدء الخادم
app.listen(config.port, () => {
  console.log(`الخادم يعمل على المنفذ ${config.port} في وضع ${config.env}`);
});
>
ملخص: يحوّل TypeScript تطوير Node.js من خلال توفير الأمان من حيث النوع، وأدوات أفضل، وقابلية صيانة محسّنة. أتقن هذه الأنماط لبناء تطبيقات Node.js قوية وجاهزة للإنتاج بثقة.