TypeScript: مشروع شامل لـ TypeScript

آخر تحديث: 2026-08-26

الآن بعد أن أكملت الدروس الـ29 الأولى، حان الوقت لتجميع كل تلك المعرفة معًا — في هذا الدرس، سنقوم بإنشاء تطبيق «Todo» يعمل عبر واجهة سطر الأوامر (CLI) من الصفر باستخدام لغة TypeScript، وسنغطي المفاهيم الأساسية مثل تصميم الأنواع، والأنواع العامة، والوحدات النمطية، ومعالجة الأخطاء.

1. نظرة عامة على المشروع

(1) المتطلبات الوظيفية

TEXT 📖 للعرض فقط
Todo CLI Tools
├── Add a Task(add)
├── List Tasks(list)
├── Complete the task(done)
├── Delete Task(delete)
├── Filter by Status(list --done / list --pending)
└── Data Persistence(JSON File Storage)

(2) المتطلبات الفنية



2. تصميم الخطوط

(1) تعريفات أنواع النوى

TYPESCRIPT
// types.ts
type Priority = "low" | "medium" | "high";
type TodoStatus = "pending" | "done";

interface Todo {
  id: string;
  title: string;
  description?: string;
  priority: Priority;
  status: TodoStatus;
  createdAt: string;
  updatedAt: string;
}

// Derivation Using Tool Types——DRY Principles
type CreateTodo = Omit<Todo, "id" | "createdAt" | "updatedAt">;
type UpdateTodo = Partial<Omit<Todo, "id" | "createdAt">>;

interface TodoFilter {
  status?: TodoStatus;
  priority?: Priority;
}

type SortBy = "createdAt" | "priority" | "title";
type SortOrder = "asc" | "desc";

interface TodoSort {
  by: SortBy;
  order: SortOrder;
}

(2) نمط النتائج

TYPESCRIPT
// result.ts
type Success<T> = { ok: true; value: T };
type Failure<E> = { ok: false; error: E };
type Result<T, E = string> = Success<T> | Failure<E>;

function ok<T>(value: T): Success<T> {
  return { ok: true, value };
}

function err<E>(error: E): Failure<E> {
  return { ok: false, error };
}


3. طبقة التخزين العامة

(1) واجهة التخزين

TYPESCRIPT
// storage.ts
interface Storage<T extends { id: string }> {
  getAll(): Result<T[]>;
  getById(id: string): Result<T | null>;
  save(item: T): Result<T>;
  delete(id: string): Result<boolean>;
  find(predicate: (item: T) => boolean): Result<T[]>;
}

(2) تنفيذ تخزين ملفات JSON

TYPESCRIPT
class JsonFileStorage<T extends { id: string }> implements Storage<T> {
  private data: Map<string, T> = new Map();
  private loaded: boolean = false;

  constructor(private filePath: string) {}

  private load(): Result<void> {
    if (this.loaded) return ok(undefined);
    try {
      let fs = require("fs");
      if (fs.existsSync(this.filePath)) {
        let raw = fs.readFileSync(this.filePath, "utf-8");
        let items: T[] = JSON.parse(raw);
        items.forEach(item => this.data.set(item.id, item));
      }
      this.loaded = true;
      return ok(undefined);
    } catch (error) {
      return err(`Failed to load:${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private persist(): Result<void> {
    try {
      let fs = require("fs");
      let items = Array.from(this.data.values());
      fs.writeFileSync(this.filePath, JSON.stringify(items, null, 2), "utf-8");
      return ok(undefined);
    } catch (error) {
      return err(`Save Failed:${error instanceof Error ? error.message : String(error)}`);
    }
  }

  getAll(): Result<T[]> {
    let r = this.load();
    if (!r.ok) return err(r.error);
    return ok(Array.from(this.data.values()));
  }

  getById(id: string): Result<T | null> {
    let r = this.load();
    if (!r.ok) return err(r.error);
    return ok(this.data.get(id) ?? null);
  }

  save(item: T): Result<T> {
    let r = this.load();
    if (!r.ok) return err(r.error);
    this.data.set(item.id, { ...item, updatedAt: new Date().toISOString() } as T);
    let p = this.persist();
    if (!p.ok) return err(p.error);
    return ok({ ...item, updatedAt: new Date().toISOString() } as T);
  }

  delete(id: string): Result<boolean> {
    let r = this.load();
    if (!r.ok) return err(r.error);
    let existed = this.data.delete(id);
    let p = this.persist();
    if (!p.ok) return err(p.error);
    return ok(existed);
  }

  find(predicate: (item: T) => boolean): Result<T[]> {
    let r = this.getAll();
    if (!r.ok) return err(r.error);
    return ok(r.value.filter(predicate));
  }
}


4. طبقة منطق الأعمال

TYPESCRIPT
// service.ts
class TodoService {
  private idCounter: number = 0;

  constructor(private storage: Storage<Todo>) {}

  private generateId(): string {
    return `todo_${Date.now()}_${++this.idCounter}`;
  }

  add(input: CreateTodo): Result<Todo> {
    let now = new Date().toISOString();
    let todo: Todo = {
      id: this.generateId(),
      title: input.title,
      description: input.description,
      priority: input.priority,
      status: "pending",
      createdAt: now,
      updatedAt: now
    };
    return this.storage.save(todo);
  }

  list(filter?: TodoFilter, sort?: TodoSort): Result<Todo[]> {
    let r = this.storage.getAll();
    if (!r.ok) return err(r.error);

    let todos = r.value;

    // Filter
    if (filter?.status) todos = todos.filter(t => t.status === filter.status);
    if (filter?.priority) todos = todos.filter(t => t.priority === filter.priority);

    // Sort
    let sortBy: SortBy = sort?.by ?? "createdAt";
    let order: SortOrder = sort?.order ?? "desc";

    let priorityOrder: Record<Priority, number> = { high: 3, medium: 2, low: 1 };

    todos.sort((a, b) => {
      let cmp = 0;
      if (sortBy === "createdAt") cmp = a.createdAt.localeCompare(b.createdAt);
      else if (sortBy === "priority") cmp = priorityOrder[a.priority] - priorityOrder[b.priority];
      else if (sortBy === "title") cmp = a.title.localeCompare(b.title);
      return order === "asc" ? cmp : -cmp;
    });

    return ok(todos);
  }

  done(id: string): Result<Todo> {
    let r = this.storage.getById(id);
    if (!r.ok) return err(r.error);
    if (!r.value) return err(`Task ${id} Does not exist`);
    if (r.value.status === "done") return err(`Task ${id} Completed`);
    return this.storage.save({ ...r.value, status: "done" });
  }

  delete(id: string): Result<boolean> {
    return this.storage.delete(id);
  }

  update(id: string, updates: UpdateTodo): Result<Todo> {
    let r = this.storage.getById(id);
    if (!r.ok) return err(r.error);
    if (!r.value) return err(`Task ${id} Does not exist`);
    return this.storage.save({ ...r.value, ...updates });
  }
}

▶ مثال: اختبار طبقة الخدمة

TYPESCRIPT
// Usage——Create a service and perform an operation
let storage = new JsonFileStorage<Todo>("./todos.json");
let service = new TodoService(storage);

// Add a Task
let addResult = service.add({ title: "StudyTypeScript", priority: "high" });
if (addResult.ok) {
  console.log(`Added:${addResult.value.title}`);
}

// List Tasks
let listResult = service.list({ status: "pending" });
if (listResult.ok) {
  console.log(`To-Do List:${listResult.value.length} items`);
  listResult.value.forEach(t => console.log(`  - ${t.title} [${t.priority}]`));
}

// Complete the task
if (addResult.ok) {
  let doneResult = service.done(addResult.value.id);
  if (doneResult.ok) {
    console.log(`Completed:${doneResult.value.title}`);
  }
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
// Executed successfully


5. طبقة العرض في واجهة سطر الأوامر (CLI)

TYPESCRIPT
// cli.ts
class TodoCLI {
  constructor(private service: TodoService) {}

  run(command: string, args: string[]): void {
    switch (command) {
      case "add": this.handleAdd(args); break;
      case "list": this.handleList(args); break;
      case "done": this.handleDone(args); break;
      case "delete": this.handleDelete(args); break;
      default: this.printHelp();
    }
  }

  private handleAdd(args: string[]): void {
    if (args.length === 0) {
      console.log("Usage:add <Title> [Priority:low|medium|high]");
      return;
    }
    let title = args[0];
    let priority: Priority =
      args[1] === "low" || args[1] === "medium" || args[1] === "high"
        ? args[1] : "medium";

    let r = this.service.add({ title, priority });
    if (r.ok) {
      console.log(`✅ Added:${r.value.title} [${r.value.priority}]`);
    } else {
      console.log(`❌ Failed to add:${r.error}`);
    }
  }

  private handleList(args: string[]): void {
    let filter: TodoFilter = {};
    if (args.includes("--done")) filter.status = "done";
    if (args.includes("--pending")) filter.status = "pending";

    let r = this.service.list(filter);
    if (!r.ok) { console.log(`❌ Query Failed:${r.error}`); return; }
    if (r.value.length === 0) { console.log("📭 No tasks"); return; }

    console.log("\n📋 Task List:");
    console.log("─".repeat(50));
    for (let t of r.value) {
      let s = t.status === "done" ? "✅" : "⬜";
      let p = { low: "🟢", medium: "🟡", high: "🔴" }[t.priority];
      console.log(`${s} ${p} [${t.id}] ${t.title}`);
    }
    console.log("─".repeat(50));
    console.log(`Total ${r.value.length} task`);
  }

  private handleDone(args: string[]): void {
    if (args.length === 0) { console.log("Usage:done <TaskID>"); return; }
    let r = this.service.done(args[0]);
    if (r.ok) console.log(`✅ Completed:${r.value.title}`);
    else console.log(`❌ Operation Failed:${r.error}`);
  }

  private handleDelete(args: string[]): void {
    if (args.length === 0) { console.log("Usage:delete <TaskID>"); return; }
    let r = this.service.delete(args[0]);
    if (r.ok && r.value) console.log("🗑️ Deleted");
    else if (r.ok) console.log("❌ The task does not exist.");
    else console.log(`❌ Deletion Failed:${r.error}`);
  }

  private printHelp(): void {
    console.log("Todo CLI Tools:");
    console.log("  add <Title> [Priority]       Add a Task");
    console.log("  list [--done|--pending]   List Tasks");
    console.log("  done <ID>                 Complete the task");
    console.log("  delete <ID>               Delete Task");
  }
}


6. ملفات الإدخال

TYPESCRIPT
// index.ts
import { TodoService } from "./service";
import { JsonFileStorage } from "./storage";
import { TodoCLI } from "./cli";
import type { Todo } from "./types";

let storage = new JsonFileStorage<Todo>("./todos.json");
let service = new TodoService(storage);
let cli = new TodoCLI(service);

let args = process.argv.slice(2);
let command = args[0] ?? "list";
let commandArgs = args.slice(1);

cli.run(command, commandArgs);


7. قائمة معارف المشروع

المفاهيم التي يتناولها هذا المشروع تتوافق مع الدروس السابقة التالية:

المفاهيم الأساسية المقرر الدراسي ذو الصلة التطبيق في المشاريع
الأنواع الأولية الدرس 4 أنواع السلاسل والقيم المنطقية في لغة Todo
استدلال الأنواع الدرس 5 تعليقات الأنواع للمتغيرات المحذوفة
أنواع الاتحاد الدرس 6 Priority، TodoStatus
المصفوفات الدرس 7 التخزين في Todo[]
نوع الكائن الدرس 8 واجهتا Todo و TodoFilter
الواجهة الدرس 9 تعريف واجهة التخزين
أنواع الدوال الدرس 10 معلمات استدعاء الدالة المساعدة
أسماء الأنواع البديلة الدرس 11 نوع النتيجة
الفصل الدرس 13 TodoService، JsonFileStorage
الفئات والواجهات الدرس 14 تنفذ واجهة Storage
الأنواع العامة الدرس 15 إضافة معلمات عامة إلى التخزين
القيود العامة الدرس 16 T يمتد إلى { id: سلسلة }
حراس الأنواع الدرس 17 تضييق نطاق result.ok
تأكيدات الأنواع الدرس 18 تأكيدات «as T»
نوع الأداة الدرس 19 حذف، جزئي، تسجيل
الوحدات الدرس 22 استيراد/تصدير الوحدات
tsconfig الدرس 24 تكوين المشروع
معالجة الأخطاء الدرس 26 نمط النتيجة

▶ مثال: تنفيذ التخزين في الذاكرة

TYPESCRIPT
class MemoryStorage<T extends { id: string }> implements Storage<T> {
  private data: Map<string, T> = new Map();

  getAll(): Result<T[]> {
    return ok(Array.from(this.data.values()));
  }

  getById(id: string): Result<T | null> {
    return ok(this.data.get(id) ?? null);
  }

  save(item: T): Result<T> {
    this.data.set(item.id, item);
    return ok(item);
  }

  delete(id: string): Result<boolean> {
    return ok(this.data.delete(id));
  }

  find(predicate: (item: T) => boolean): Result<T[]> {
    let all = Array.from(this.data.values());
    return ok(all.filter(predicate));
  }
}

let memStore = new MemoryStorage<Todo>();
let memService = new TodoService(memStore);

let r = memService.add({ title: "Demo task", priority: "high" });
if (r.ok) console.log(`Added: ${r.value.title}`);
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
Added: Demo task

▶ مثال: بنية طبقات مع حقن التبعيات

TYPESCRIPT
interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

class SilentLogger implements Logger {
  log(_message: string): void { }
}

class App {
  constructor(
    private service: TodoService,
    private logger: Logger
  ) {}

  run(command: string): void {
    this.logger.log(`Executing: ${command}`);
    let result = this.service.list();
    if (result.ok) {
      this.logger.log(`Found ${result.value.length} items`);
    }
  }
}

let app = new App(
  new TodoService(new MemoryStorage<Todo>()),
  new ConsoleLogger()
);
app.run("list");
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
[LOG] Executing: list
[LOG] Found 0 items

❓ أسئلة شائعة

س ما هي الميزات التي يمكن إضافتها إلى هذا المشروع؟
ج المجالات المقترحة للتوسع: (1) التخزين غير المتزامن — التبديل إلى async/await (2) نظام العلامات — إضافة علامات string[] إلى المهام (3) المهام الفرعية — يمكن أن تحتوي المهام على مهام فرعية (4) التصدير إلى CSV/Markdown (5) دعم القوائم المتعددة. يُعد كل امتداد تمرينًا رائعًا لترسيخ ما تعلمته في الدروس السابقة.
س لماذا نستخدم نمط Result بدلاً من try/catch؟
ج في أدوات واجهة سطر الأوامر (CLI)، تكون الأخطاء أخطاءً تشغيلية يمكن توقعها (مثل عدم وجود مهمة ما). ويوضح نمط Result في توقيع الدالة أن العملية «قد تفشل»، ويجب على المستدعي التعامل معها. وهو أكثر ملاءمة لهذا السيناريو من try/catch — فالخطأ ليس «استثناءً» بل «فرعًا عاديًا».
س لماذا تستخدم طبقة التخزين الأنواع العامة بدلاً من استخدام Todo مباشرةً؟
ج الأنواع العامة تجعل طبقة التخزين قابلة لإعادة الاستخدام — فمن خلال إضافة معلمات عامة، يمكن لـ JsonFileStorage تخزين أي كيان يحتوي على خاصية id. ومع نمو المشروع، لا توجد حاجة لكتابة تطبيق تخزين جديد لكل كيان. هذه هي القيمة الأساسية للأنواع العامة — اكتب مرة واحدة، واستخدم في كل مكان.
س كيف يمكنني تحويل مشروع إلى واجهة برمجة تطبيقات ويب (Web API)؟
ج تظل البنية الأساسية دون تغيير — ما عليك سوى استبدال طبقة واجهة سطر الأوامر (CLI) بطبقة توجيه Express. ويتم إعادة استخدام طبقتي التخزين والخدمة بالكامل. وهذه هي ميزة البنية الطبقية — حيث يمكن استبدال طبقة العرض دون التأثير على منطق الأعمال.

📖 ملخص

📝 تمارين

  1. تمرين أساسي (مستوى الصعوبة: ⭐): قسّم كود المشروع إلى ملفات .ts منفصلة حسب كل وحدة، وقم بتكوين ملف tsconfig.json، وتأكد من نجاح ترجمة tsc.
  2. تمرين متقدم (مستوى الصعوبة ⭐⭐): أضف الميزة tag إلى TodoService — وأضف الخاصية tags: string[] إلى Todo لدعم التصفية حسب العلامات list --tag <labelName>. ستحتاج إلى تعديل تعريف النوع، وطبقة التخزين، وطبقة واجهة سطر الأوامر (CLI).
  3. التحدي (الصعوبة: ⭐⭐⭐): قم بتحويل التخزين المتزامن إلى تخزين غير متزامن — قم بتغيير جميع الطرق في Storage<T> إلى async واجعلها تُرجع Promise<Result<T>>. كما يجب تحويل طبقتي الخدمة وواجهة سطر الأوامر (CLI) إلى async/await. تعامل مع الأخطاء غير المتزامنة وتأكد من سلامة الأنواع.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%