TypeScript مقابل JavaScript: دليل Go-to الخاص بك
نشرت: 2022-03-11TypeScript أو JavaScript؟ يفكر المطورون في هذا الاختيار لمشاريع Greenfield على الويب أو Node.js ، لكن هذا سؤال يستحق النظر فيه بالنسبة للمشاريع الحالية أيضًا. مجموعة شاملة من JavaScript ، توفر TypeScript جميع ميزات JavaScript بالإضافة إلى بعض الامتيازات الإضافية. تشجعنا TypeScript جوهريًا على البرمجة بشكل واضح ، مما يجعل الكود أكثر قابلية للتوسع. ومع ذلك ، يمكن أن تحتوي المشاريع على الكثير من جافا سكريبت عادي كما نحب ، لذا فإن استخدام TypeScript ليس اقتراح الكل أو لا شيء.
العلاقة بين TypeScript و JavaScript
تضيف TypeScript نظام كتابة صريح إلى JavaScript ، مما يسمح بفرض صارم لأنواع المتغيرات. يقوم TypeScript بتشغيل عمليات التحقق من النوع الخاص به أثناء التحويل - وهو شكل من أشكال التحويل البرمجي يحول تعليمات TypeScript البرمجية إلى متصفحات الويب الخاصة بتعليمات JavaScript البرمجية ويفهم Node.js.
TypeScript مقابل أمثلة JavaScript
لنبدأ بمقتطف JavaScript صالح:
let var1 = "Hello"; var1 = 10; console.log(var1); هنا ، يبدأ var1 string ، ثم يصبح number .
نظرًا لأن JavaScript مكتوب بشكل فضفاض فقط ، يمكننا إعادة تعريف var1 كمتغير من أي نوع - من سلسلة إلى دالة - في أي وقت.
تنفيذ مخرجات هذا الكود 10 .
الآن ، دعنا نغير هذا الرمز إلى TypeScript:
let var1: string = "Hello"; var1 = 10; console.log(var1); في هذه الحالة ، نعلن أن var1 string . نحاول بعد ذلك تعيين رقم لها ، وهو ما لا يسمح به نظام الكتابة الصارم من TypeScript. ينتج عن النقل خطأ:
TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10; إذا طلبنا من المترجم أن يتعامل مع مقتطف JavaScript الأصلي كما لو كان TypeScript ، فسيستنتج المترجم تلقائيًا أن var1 يجب أن يكون string | number string | number . هذا هو نوع اتحاد TypeScript ، والذي يسمح لنا بتعيين var1 string أو number في أي وقت. بعد حل تعارض النوع ، سيتم تحويل شفرة TypeScript الخاصة بنا بنجاح. سيؤدي تنفيذه إلى نفس النتيجة مثل مثال JavaScript.
TypeScript مقابل JavaScript بدءًا من 30000 قدم: تحديات قابلية التوسع
جافا سكريبت موجودة في كل مكان ، وتعمل على تشغيل المشاريع من جميع الأحجام ، ويتم تطبيقها بطرق لم يكن من الممكن تصورها خلال طفولتها في التسعينيات. بينما نضجت JavaScript ، فإنها تقصر عندما يتعلق الأمر بدعم قابلية التوسع. وفقًا لذلك ، يتعامل المطورون مع تطبيقات JavaScript التي نمت من حيث الحجم والتعقيد.
لحسن الحظ ، يعالج TypeScript العديد من مشكلات تحجيم مشاريع JavaScript. سنركز على أهم ثلاثة تحديات: التحقق من الصحة وإعادة البناء والتوثيق.
تصديق
نحن نعتمد على بيئات التطوير المتكاملة (IDEs) للمساعدة في مهام مثل إضافة وتعديل واختبار رمز جديد ، لكن IDE لا يمكنه التحقق من صحة مراجع JavaScript البحتة. نخفف من هذا القصور من خلال المراقبة اليقظة لأننا نبرمج لدرء احتمال حدوث أخطاء مطبعية في المتغيرات وأسماء الوظائف.
ينمو حجم المشكلة بشكل كبير عندما تنشأ الشفرة من طرف ثالث ، حيث يمكن بسهولة عدم اكتشاف المراجع المعطلة في فروع الكود التي نادرًا ما يتم تنفيذها.
في المقابل ، مع TypeScript ، يمكننا تركيز جهودنا على الترميز ، واثقين من أنه سيتم تحديد أي أخطاء في وقت التحويل. لتوضيح ذلك ، لنبدأ ببعض تعليمات JavaScript البرمجية القديمة:
const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } } .toISO() هو خطأ مطبعي في اللحظة. js طريقة toISOString() لكن الكود سيعمل ، بشرط ألا تكون وسيطة format هي ISO . في المرة الأولى التي نحاول فيها تمرير ISO إلى الوظيفة ، سيؤدي ذلك إلى رفع خطأ وقت التشغيل هذا: TypeError: moment(...).toISO is not a function .
قد يكون من الصعب تحديد موقع التعليمات البرمجية التي بها أخطاء إملائية. قد لا يكون لقاعدة الشفرة الحالية مسار للخط المقطوع ، وفي هذه الحالة لن يتم اكتشاف مرجع .toISO() عن طريق الاختبار.
إذا قمنا بنقل هذا الرمز إلى TypeScript ، فسوف يقوم IDE بتمييز المرجع المكسور ، مما يدفعنا إلى إجراء تصحيحات. إذا لم نفعل شيئًا وحاولنا التحويل ، فسيتم حظرنا ، وسيولد المترجم الخطأ التالي:
TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());إعادة بناء التعليمات البرمجية
على الرغم من أن الأخطاء الإملائية في مراجع التعليمات البرمجية لجهات خارجية ليست شائعة ، إلا أن هناك مجموعة مختلفة من المشكلات المرتبطة بالأخطاء الإملائية في المراجع الداخلية ، مثل هذا:
const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); } يمكن للمطور الوحيد تحديد موقع وإصلاح جميع مثيلات phoneNumbr لتنتهي بـ er بسهولة كافية.
لكن كلما زاد حجم الفريق ، زادت تكلفة هذا الخطأ الشائع البسيط بشكل غير معقول. أثناء أداء عملهم ، سيحتاج الزملاء إلى إدراك مثل هذه الأخطاء المطبعية ونشرها. بدلاً من ذلك ، فإن إضافة رمز لدعم كلا التهجئات سيؤدي إلى تضخم قاعدة التعليمات البرمجية دون داع.
باستخدام TypeScript ، عندما نصلح خطأ إملائيًا ، لن يتم تحويل الشفرة التابعة بعد الآن ، مما يشير إلى الزملاء لنشر الإصلاح إلى الكود الخاص بهم.
توثيق
التوثيق الدقيق وذات الصلة هو مفتاح الاتصال داخل وبين فرق المطورين. غالبًا ما يستخدم مطورو JavaScript JSDoc لتوثيق الطرق وأنواع الخصائص المتوقعة.
تسهل ميزات لغة TypeScript (مثل الفئات المجردة والواجهات وتعريفات النوع) برمجة التصميم حسب العقد ، مما يؤدي إلى توثيق الجودة. علاوة على ذلك ، فإن وجود تعريف رسمي للطرق والخصائص التي يجب أن يلتزم بها الكائن يساعد في تحديد التغييرات الفاصلة ، وإنشاء الاختبارات ، وإجراء استبطان التعليمات البرمجية ، وتنفيذ الأنماط المعمارية.
بالنسبة إلى TypeScript ، تقوم أداة الانتقال TypeDoc (استنادًا إلى اقتراح TSDoc) باستخراج معلومات الكتابة تلقائيًا (على سبيل المثال ، الفئة والواجهة والطريقة والممتلكات) من التعليمات البرمجية الخاصة بنا. وبالتالي ، فإننا ننشئ وثائق تكون ، إلى حد بعيد ، أكثر شمولاً من تلك الخاصة بـ JSDoc.
مزايا TypeScript مقابل JavaScript
الآن ، دعنا نستكشف كيف يمكننا استخدام TypeScript لمواجهة تحديات قابلية التوسع هذه.
كود متقدم / اقتراحات إعادة بناء ديون
يمكن للعديد من IDEs معالجة المعلومات من نظام TypeScript ، مما يوفر التحقق من صحة المرجع أثناء قيامنا بالتشفير. والأفضل من ذلك ، عندما نكتب ، يمكن لـ IDE تقديم وثائق ذات صلة سريعة (على سبيل المثال ، الحجج التي تتوقعها الوظيفة) لأي مرجع وتقترح أسماء متغيرات صحيحة من حيث السياق.
في مقتطف TypeScript هذا ، يقترح IDE إكمالًا تلقائيًا لأسماء المفاتيح داخل القيمة المرجعة للوظيفة:
/** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }قدم IDE الخاص بي ، رمز Visual Studio ، هذا الاقتراح (في وسيلة الشرح) عندما بدأت في استدعاء الوظيفة (السطر 31):
علاوة على ذلك ، فإن اقتراحات الإكمال التلقائي لـ IDE (في وسيلة الشرح) صحيحة من حيث السياق ، حيث تعرض الأسماء الصالحة فقط في حالة المفتاح المتداخلة (السطر 34):
تؤدي مثل هذه الاقتراحات في الوقت الفعلي إلى ترميز أسرع. علاوة على ذلك ، يمكن أن تعتمد IDE على معلومات النوع الصارمة الخاصة بـ TypeScript لإعادة بناء الكود على أي مقياس. تصبح العمليات مثل إعادة تسمية خاصية أو تغيير مواقع الملفات أو حتى استخراج فئة فائقة تافهة عندما نكون واثقين بنسبة 100٪ في دقة مراجعنا.

دعم الواجهة
على عكس JavaScript ، يوفر TypeScript القدرة على تحديد الأنواع باستخدام الواجهات . تسرد الواجهة بشكل رسمي - ولكنها لا تنفذ - الأساليب والخصائص التي يجب أن يتضمنها الكائن. بناء اللغة هذا مفيد بشكل خاص للتعاون مع المطورين الآخرين.
يوضح المثال التالي كيف يمكننا الاستفادة من ميزات TypeScript لتنفيذ أنماط OOP الشائعة بدقة - في هذه الحالة ، الاستراتيجية وسلسلة المسؤولية - مما يؤدي إلى تحسين المثال السابق:
export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }وحدات ES6 - في أي مكان
حتى كتابة هذه السطور ، لا تدعم جميع أوقات تشغيل JavaScript للواجهة الأمامية والخلفية وحدات ES6. ومع ذلك ، باستخدام TypeScript ، يمكننا استخدام بناء جملة الوحدة النمطية ES6:
import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c'])); سيكون الإخراج المترجم متوافقًا مع البيئة المحددة لدينا. على سبيل المثال ، باستخدام خيار المترجم --module CommonJS ، نحصل على:
"use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; باستخدام --module UMD بدلاً من ذلك ، يقوم TypeScript بإخراج نمط UMD الأكثر تفصيلاً:
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });فئات ES6 - في أي مكان
غالبًا ما تفتقر البيئات القديمة إلى دعم فصول ES6. يضمن النقل من نوع TypeScript التوافق باستخدام التركيبات الخاصة بالهدف. إليك مقتطف مصدر TypeScript:
export class TestClass { hello = 'World'; }يعتمد إخراج JavaScript على كل من الوحدة والهدف ، والذي يتيح لنا TypeScript تحديده.
إليك ما ينتج عن --module CommonJS --target es3 :
"use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass; باستخدام --module CommonJS --target es6 بدلاً من ذلك ، نحصل على النتيجة المترجمة التالية. تُستخدم الكلمة الأساسية class لاستهداف ES6:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;وظيفة عدم التزامن / انتظار - في أي مكان
يؤدي عدم التزامن / انتظار إلى تسهيل فهم رمز JavaScript غير المتزامن والحفاظ عليه. يوفر TypeScript هذه الوظيفة لجميع أوقات التشغيل ، حتى تلك التي لا توفر عدم التزامن / الانتظار أصلاً.
لاحظ أنه للتشغيل غير المتزامن / الانتظار في أوقات التشغيل الأقدم مثل ES3 و ES5 ، ستحتاج إلى دعم خارجي للإخراج المستند إلى Promise (على سبيل المثال ، عبر Bluebird أو ES2015 polyfill). يتكامل Promise polyfill الذي يأتي مع TypeScript بسهولة في المخرجات المنقولة - نحتاج فقط إلى تكوين خيار lib compiler وفقًا لذلك.
دعم حقول الفصل الخاص - في أي مكان
حتى بالنسبة للأهداف القديمة ، يدعم TypeScript الحقول private بنفس الطريقة التي تدعم بها اللغات المكتوبة بشدة (على سبيل المثال ، Java أو C #). في المقابل ، تدعم العديد من أوقات تشغيل JavaScript الحقول private من خلال بناء جملة بادئة التجزئة ، وهو اقتراح نهائي لـ ES2022.
عيوب TypeScript مقابل JavaScript
الآن بعد أن سلطنا الضوء على الفوائد الرئيسية لتطبيق TypeScript ، دعنا نستكشف السيناريوهات التي قد لا يكون فيها TypeScript مناسبًا.
Transpilation: احتمال عدم توافق سير العمل
قد تكون مهام سير العمل أو متطلبات المشروع المحددة غير متوافقة مع خطوة الترجمة في TypeScript: على سبيل المثال ، إذا احتجنا إلى استخدام أداة خارجية لتغيير الكود بعد النشر أو إذا كان يجب أن يكون الناتج الذي تم إنشاؤه مناسبًا للمطور.
على سبيل المثال ، قمت مؤخرًا بكتابة وظيفة AWS Lambda لبيئة Node.js. كان TypeScript غير ملائم لأن طلب الترجمة سيمنعني أنا وأعضاء الفريق الآخرين من تحرير الوظيفة باستخدام محرر AWS عبر الإنترنت. كان هذا بمثابة كسر للصفقة لمدير المشروع.
اكتب نظام يعمل فقط حتى وقت Transpile
لا يحتوي إخراج JavaScript الخاص بـ TypeScript على معلومات الكتابة ، لذلك لن يقوم بإجراء فحوصات على النوع ، وبالتالي ، يمكن أن تنكسر أمان الكتابة في وقت التشغيل. على سبيل المثال ، افترض أنه تم تعريف دالة لإرجاع كائن دائمًا. إذا تم إرجاع قيمة null من استخدامه داخل ملف .js ، فسيحدث خطأ في وقت التشغيل.
اكتب الميزات المعتمدة على المعلومات (مثل الحقول الخاصة أو الواجهات أو الأدوية الجنسية) تضيف قيمة إلى أي مشروع ولكن يتم كشطها أثناء النقل. على سبيل المثال ، لم يعد أعضاء الفصل private يتمتعون بالخصوصية بعد الترجمة. لكي نكون واضحين ، فإن مشكلات وقت التشغيل من هذا النوع ليست فريدة بالنسبة إلى TypeScript ، ويمكنك أن تتوقع مواجهة نفس الصعوبات مع JavaScript أيضًا.
الجمع بين TypeScript و JavaScript
على الرغم من فوائد TypeScript العديدة ، لا يمكننا في بعض الأحيان تبرير تحويل مشروع JavaScript كامل دفعة واحدة. لحسن الحظ ، يمكننا أن نحدد لمحول TypeScript - على أساس كل ملف على حدة - ما يجب تفسيره على أنه JavaScript عادي. في الواقع ، يمكن أن يساعد هذا النهج الهجين في التخفيف من التحديات الفردية عند ظهورها على مدار دورة حياة المشروع.
قد نفضل ترك JavaScript دون تغيير إذا كانت الشفرة:
- تمت كتابته بواسطة زميل سابق وسيتطلب جهودًا كبيرة في الهندسة العكسية للتحويل إلى TypeScript.
- يستخدم أسلوب (أساليب) غير مسموح به في TypeScript (على سبيل المثال ، يضيف خاصية بعد إنشاء مثيل للكائن) وقد يتطلب إعادة بناء ديون للالتزام بقواعد TypeScript.
- ينتمي إلى فريق آخر يواصل استخدام JavaScript.
في مثل هذه الحالات ، يعطي ملف التصريح (ملف .d.ts ، الذي يطلق عليه أحيانًا ملف التعريف أو ملف الكتابة) TypeScript بيانات كتابة كافية لتمكين اقتراحات IDE مع ترك كود JavaScript كما هو.
توفر العديد من مكتبات JavaScript (على سبيل المثال ، Lodash و Jest و React) ملفات كتابة TypeScript في حزم كتابة منفصلة ، بينما تدمج مكتبات أخرى (مثل Moment.js و Axios و Luxon) ملفات الكتابة في الحزمة الرئيسية.
TypeScript مقابل JavaScript: سؤال حول التبسيط وقابلية التوسع
يعمل الدعم والمرونة والتحسينات التي لا مثيل لها المتوفرة من خلال TypeScript على تحسين تجربة المطور بشكل كبير ، مما يمكّن المشاريع والفرق من التوسع. التكلفة الرئيسية لدمج TypeScript في المشروع هي إضافة خطوة بناء الترجمة. بالنسبة لمعظم التطبيقات ، لا يمثل التحويل إلى JavaScript مشكلة ؛ بدلاً من ذلك ، إنها نقطة انطلاق للفوائد العديدة لـ TypeScript.
مزيد من القراءة على مدونة Toptal Engineering:
- العمل مع دعم TypeScript و Jest: برنامج تعليمي لـ AWS SAM
