"ليس setTimeout الصحيح": كيف يدفع TypeScript المطورين إلى الجنون
\nهل سبق لك أن كتبت مؤقتًا بسيطًا بلغة TypeScript، وفجأة بدأ محرر الكود بوضع خطوط حمراء تحت كل شيء؟ مشهد مألوف: تقوم باستيراد setTimeout، وتكتب const timer = setTimeout(...)، ويظهر TypeScript خطأً أو يقترح نوعًا غير مفهوم. ما المشكلة؟ اتضح أن setTimeout له نسختان - واحدة للمتصفح (window) والأخرى للخادم (Node.js). و TypeScript، بصفته مثالياً، يجبرنا على الاختيار.
هذا الموضوع هو واحد من أكثر المواضيع شيوعًا على Stack Overflow. آلاف المطورين يتعثرون بنفس المشكلة. لكننا لن نقوم فقط بنسخ الإجابة. سنفهم جذر المشكلة، وكيف تعمل كتابة الأنواع، ونتعلم كتابة كود لا ينكسر لا في المتصفح ولا في Node.js.
\n\nلماذا توجد نسختان من setTimeout؟
\nتاريخيًا، يعيش JavaScript في عالمين: العميل (المتصفح) والخادم (Node.js). في المتصفح، setTimeout هي طريقة للكائن العام window. في Node.js، هي دالة من الوحدة العامة timers. كلاهما يفعل نفس الشيء، لكن توقيعات أنواعهما مختلفة.
إليك الاختلافات الرئيسية:
\n- \n
- القيمة المُرجعة: في المتصفح،
window.setTimeoutيُرجعnumber(معرف المؤقت). في Node.js، يُرجع كائنًا من نوعNodeJS.Timeout. \n - السياق: في المتصفح،
thisداخل callback هوwindow(في الوضع غير الصارم). في Node.js، هو كائن المؤقت. \n - كتابة الأنواع: يوفر TypeScript تصريحات مختلفة لبيئات مختلفة. إذا كنت تكتب كودًا عالميًا (تطبيق متشابه الشكل)، ينشأ تعارض. \n
كيف تبدو المشكلة في الكود
\nتخيل أنك تطور مكتبة يجب أن تعمل على كل من العميل والخادم. تكتب الكود التالي:
\n// utils.ts\nexport function delay(ms: number): Promise<void> {\n return new Promise(resolve => {\n const timer = setTimeout(resolve, ms);\n // ما نوع timer؟ number أم NodeJS.Timeout؟\n });\n}\nسيرى TypeScript أن setTimeout يمكن أن يكون من نوعين، وسيظهر خطأ: "Type 'Timeout' is not assignable to type 'number'." أو العكس، اعتمادًا على إعدادات tsconfig.json والأنواع المتصلة.
لتوضيح الأمر أكثر، إليك كيف تبدو الأنواع عند النظر إليها من خلال ReturnType:
// في المتصفح (lib.dom.d.ts)\ntype BrowserTimer = ReturnType<typeof window.setTimeout>; // number\n\n// في Node.js (@types/node)\ntype NodeTimer = ReturnType<typeof setTimeout>; // NodeJS.Timeout\n\nالحلول الصحيحة لسيناريوهات مختلفة
\nدعنا نستعرض أربعة طرق. اختر الطريقة التي تناسب مشروعك.
\n\n1. إذا كنت تكتب فقط للمتصفح
\nأبسط حالة. فقط حدد النوع number واستخدم window.setTimeout بشكل صريح:
// browser-timer.ts\nconst timerId: number = window.setTimeout(() => {\n console.log('لقد مرت ثانية');\n}, 1000);\n\n// للإزالة\nwindow.clearTimeout(timerId);\nلماذا يعمل هذا؟ في بيئة المتصفح، window.setTimeout يُرجع دائمًا رقمًا. TypeScript يعرف ذلك إذا كان لديك lib: ["dom"] متصلاً.
2. إذا كنت تكتب فقط لـ Node.js
\nهنا النوع هو NodeJS.Timeout. استخدمه مباشرة:
// node-timer.ts\nimport { setTimeout } from 'timers'; // استيراد صريح للوضوح\n\nconst timer: NodeJS.Timeout = setTimeout(() => {\n console.log('تم تشغيل المؤقت');\n}, 2000);\n\nclearTimeout(timer); // clearTimeout يقبل NodeJS.Timeout\nهام: في Node.js، clearTimeout يقبل أيضًا No