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) المتطلبات الفنية
- لغة TypeScript خالصة، بدون تبعيات خارجية
- التصميم المعياري — ترتيب الطبقات حسب الأنواع، والتخزين، والمنطق التجاري، وواجهة سطر الأوامر (CLI)
- أمان كامل في أنواع البيانات — لا يوجد
any - التعامل مع الأخطاء في وضع النتائج
- طبقة تخزين عامة قابلة لإعادة الاستخدام
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. ويتم إعادة استخدام طبقتي التخزين والخدمة بالكامل. وهذه هي ميزة البنية الطبقية — حيث يمكن استبدال طبقة العرض دون التأثير على منطق الأعمال.
📖 ملخص
- ابدأ بتصميم الخطوط — حدد الخطوط الأساسية مثل
TodoوCreateTodoوUpdateTodoوResult - استنباط المتغيرات من «Todo» باستخدام أنواع الأدوات (Omit، Partial)، وفقًا لمبدأ DRY
- يمكن إعادة استخدام طبقة التخزين العامة
Storage<T extends { id: string }>لأي كيان - يجعل نمط «Result» معالجة الأخطاء صريحةً — حيث تنص توقيعات الدالة على أنها «قد تفشل»، ويجب على المستدعي معالجة الخطأ.
- بنية متعددة الطبقات: طبقة الأنواع → طبقة التخزين → طبقة الخدمات → طبقة العرض — لكل طبقة مسؤوليات محددة بوضوح، وهي منفصلة عن الطبقات الأخرى
- يدمج معظم المفاهيم التي تم تناولها في الدرس 29 — بدءًا من الأنواع الأساسية مرورًا بالأنواع العامة وصولاً إلى معالجة الأخطاء
📝 تمارين
- تمرين أساسي (مستوى الصعوبة: ⭐): قسّم كود المشروع إلى ملفات .ts منفصلة حسب كل وحدة، وقم بتكوين ملف tsconfig.json، وتأكد من نجاح ترجمة
tsc. - تمرين متقدم (مستوى الصعوبة ⭐⭐): أضف الميزة
tagإلى TodoService — وأضف الخاصيةtags: string[]إلى Todo لدعم التصفية حسب العلاماتlist --tag <labelName>. ستحتاج إلى تعديل تعريف النوع، وطبقة التخزين، وطبقة واجهة سطر الأوامر (CLI). - التحدي (الصعوبة: ⭐⭐⭐): قم بتحويل التخزين المتزامن إلى تخزين غير متزامن — قم بتغيير جميع الطرق في
Storage<T>إلىasyncواجعلها تُرجعPromise<Result<T>>. كما يجب تحويل طبقتي الخدمة وواجهة سطر الأوامر (CLI) إلىasync/await. تعامل مع الأخطاء غير المتزامنة وتأكد من سلامة الأنواع.