التعمق في ملفات التصريحات في TypeScript

من موسوعة حسوب
اذهب إلى: تصفح، ابحث

التعمق في نظرية ملفّات التعريف (Definition File Theory)

هيكلة الوحدات لتحمل نفس شكل الواجهة البرمجية (API) الذي تريده بالضبطِ صعبٌ نوعًا ما. على سبيل المثال، قد نرغب من وحدةٍ قد تُستدعى بالكلمة المفتاحية ‎new‎ أو بدونها أن تنتج أنواعًا مختلفة، مع امتلاك عدّة أنواعٍ مسمّاةٍ موجودة في تسلسلٍ ما، وبعض الخاصيات على كائن الوحدة كذلك.

عبر قراءة هذا الدليل، ستحصل على الأدوات اللازمة لكتابة ملفّات تعريفات معقّدة ذات واجهة برمجيّة سهلة الاستخدام. يركّز هذا الدليل على مكتبات الوحدات أو مكتبات UMD لأنّ الخيارات هنا تكون أكثر تنوّعًا.

المبادئ الأساسيّة

يمكنك استيعاب كيفيّة إنشاء أي شكلِ تعريفاتٍ عبر فهم بعض المبادئ الأساسيّة لآليّة عمل TypeScript.

الأنواع

إذا كنت تقرأ هذا الدليل، فأغلب الظن أنك تدري ماهية الأنواع في TypeScript. ولفهم ذلك أكثر، عليك أن تدرك أنّه يُمكن إنشاء نوعٍ عبر ما يلي:

  • تصريح تسمية نوع بديلةٍ (type alias declaration)، كما في المثال: ‎type sn = number | string;‎.
  • تصريح واجهةٍ كما في المثال: ‎interface I { x: number[]; }‎.
  • تصريح صنفٍ كما في المثال: ‎class C { }‎.
  • تصريح ثابت متعدّدٍ (enum) كما في المثال: ‎enum E { A, B, C }‎.
  • تصريح استيرادِ ‎import‎ يُشير إلى نوعٍ معيّن.

ينشئ كل شكلٍ من أشكال التصريحات هذه اسمَ نوعٍ جديدٍ.‎

القيم

القيم مألوفة بالنسبة لك كذلك. القيم هي أسماءٌ تتواجد أثناء التنفيذ يُمكننا الإشارة إليها في التعابير. على سبيل المثال، يُنشئ التعبير ‎let x = 5;‎ قيمةً تُسمّى ‎x‎.

بشكلٍ أوضح، تُنشَأُ القيم بما يلي:

  • تصريحات ‎let‎، و‎const‎، و‎var‎.
  • تصريح ‎namespace‎ يحتوي على قيمة، أو تصريح ‎module‎ يحتوي على قيمة.
  • تصريح ‎enum‎.
  • تصريح ‎class‎.
  • تصريح ‎import‎ يشير إلى قيمة.
  • تصريح ‎function‎.

أسماء المجالات

يمكن للأنواع أن تتواجد داخل أسماء المجالات. مثلًا، إن كان لدينا التصريح ‎let x: A.B.C‎، فإنّنا نقول أنّ النوع ‎C‎ يأتي من مجال الأسماء ‎A.B‎. الفرق طفيف لكنه مهم، هنا A.B ليس بالضرورة نوعًا أو قيمةً.

التجميعات البسيطة: اسمٌ واحد ذو معانٍ متعدّدة

لنفترض أن لدينا اسمًا ‎A‎، يمكن أن نجد ثلاثة معانٍ للاسم ‎A‎: نوع، أو قيمة، أو مجال أسماء. يعتمد تفسير الاسم على السياق الذي استُخدِمَ فيه. على سبيل المثال، في التصريح ‎let m: A.A = A;‎، يُستعمَل ‎A‎ أولًا كمجال أسماءٍ، ثمّ كاسم نوعٍ، ثمّ كقيمةٍ. قد تشير هذه المعاني إلى تصريحات مختلفة تمامًا!

قد يبدو هذا محيّرًا، لكنّه مفيد جدا ما دمنا لا نستعمل هذه الميّزة زيادةً عن اللازم. لنلق نظرةً على جوانب مفيدة لسلوك التجميع هذا.

التجميعات المضمَّنة

لاحظ أنّ ‎class‎ مثلًا قد تواجد في كل من قائمة الأنواع وقائمة القيم في الآن ذاته. ينشئ التصريحُ ‎class C { }‎ شيئين اثنين: نوعٌ باسم ‎C‎ يشير إلى شكل نسخة (instance shape) الصنف، وقيمةٌ باسم ‎C‎ تشير إلى الدالة البانية (constructor function) الخاصة بالصنف. وتتصرّف تصريحات الثوابت المتعددة بشكل مشابه.

التجميعات التي تُنشأ من طرف المبرمج

لنقل أنّنا كتبنا ملفّ وحدةٍ باسم ‎foo.d.ts‎:

export var SomeVar: { a: SomeType };
export interface SomeType {
  count: number;
}

ثمّ استخدمنا الملفّ:

import * as foo from './foo';
let x: foo.SomeType = foo.SomeVar.a;
console.log(x.count);

يعمل هذا بشكل جيّد، لكن تخيّل أنّ ‎SomeType‎ و‎SomeVar‎ متعلقان ببعضهما البعض بحيث نريد أن يحملا نفس الاسم. يُمكننا استخدام التجميع لتمثيل هاذين الكائنين (القيمة والنوع) في اسم واحدٍ ‎Bar‎:

export var Bar: { a: Bar };
export interface Bar {
  count: number;
}

هذا يمثل فرصة جيدة لاستعمال التفكيك (destructuring) في الشيفرة التي تستعمل الملف:

import { Bar } from './foo';
let x: Bar = Bar.a;
console.log(x.count);

استعملنا هنا مجدّدًا ‎Bar‎ كنوعٍ وقيمةٍ في نفس الوقت. لاحظ أنّنا لا نحتاج إلى التصريح عن القيمة ‎Bar‎ على أنّها من النوع ‎Bar‎، إذ أنّ كلا منهما مستقل عن الآخر.

تجميعات متقدمة

يمكن تجميع بعض التصريحات عبر عدّة تصريحات. مثلًا، يمكن للتصريح ‎class C { }‎ أن يتواجد في نفس مجال التصريح ‎interface C { }‎ وكلاهما يُضيف خاصيات إلى النوع ‎C‎.

هذا مسموح به ما دام لا يحدث صراعًا، القاعدة العامة هنا هي أنّ القيم دائمًا ما تتصارع مع قيم أخرى ذات نفس الاسم إلا إن صُرِّح عنها كمجالات أسماء، والأنواع تتصارع إذا صُرِّح عنها بتصريح تسمية نوع بديلة (‎type s = string‎)، ومجالات الأسماء لا تتصارع أبدًا.

لننظر كيف يمكننا استغلال هذه الميّزة.

إضافة عناصر باستخدام تصريحات الواجهات ‎interface

يمكننا إضافة عناصر أخرى إلى واجهةٍ باستخدام تصريح واجهةٍ آخر:

interface Foo {
  x: number;
}
// ... في مكان آخر ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // مسموح

تعمل هذه الميّزة مع الأصناف كذلك:

class Foo {
  x: number;
}
// ... في مكان آخر ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // مسموح

لاحظ أننا لا نستطيع إضافة عناصر جديدة إلى أسماء الأنواع البديلة (‎type s = string;‎) باستخدام الواجهات.

إضافة عناصر باستخدام تصريحات مجالات الأسماء ‎namespace

يمكن استخدام تصريح مجال أسماء لإضافة أنواعٍ جديدة، أو قيمٍ، أو مجالات أسماءٍ بأية طريقة ما دامت لا تُنشئ صراعًا.

على سبيل المثال، يمكننا إضافة عنصر ساكن إلى صنف كما يلي:

class C {
}
// ... في مكان آخر ...
namespace C {
  export let x: number;
}
let y = C.x; // مسموح

لاحظ أنّنا في هذا المثال نضيف قيمة إلى الجانب الساكن (static side) من الصنف ‎C‎ (أي دالّته البانية). هذا لأنّنا أضفنا قيمةً إليه، والحاوية التي تحتوي على جميع القيم هي نفسها قيمةٌ كذلك (الأنواع تحتويها مجالات الأسماء، ومجالات الأسماء تحتويها مجالات أسماءٍ أخرى).

يمكننا كذلك إضافة نوعٍ موجودٍ داخل مجال أسماءٍ إلى صنفٍ كالتالي:

class C {
}
// ... في مكان آخر ...
namespace C {
  export interface D { }
}
let y: C.D; // مسموح

في هذا المثال، لا يوجد مجال أسماء باسم ‎C‎ حتى حينِ كتابة تصريح ‎namespace‎ له. معنى ‎C‎ كمجال أسماء لا يتصارع مع القيمة ‎C‎ أو النوع ‎C‎ اللذان أنشأهما الصنف.

وفي الأخير، يمكننا تأدية عدّة أشكال من أشكال الدمج باستخدام تصريحات ‎namespace‎. ما يلي ليس مثالًا واقعيًّا، لكنه يعرض أشكالًا مختلفة من السلوكات المثيرة للاهتمام:

namespace X {
  export interface Y { }
  export class Z { }
}

// ... في مكان آخر ...
namespace X {
  export var Y: number;
  export namespace Z {
    export class C { }
  }
}
type X = string;

في هذا المثال، كتلة الشيفرة الأولى تنشئ معاني الأسماء التاليّة:

  • قيمةٌ ‎X‎ (لأنّ ‎تصريح مجال الأسماء يحتوي على قيمةٍ ‎Z‎).
  • مجالُ أسماءٍ ‎X‎ (لأن تصريح مجال الأسماء يحتوي على نوعٍ ‎Y‎).
  • نوعٌ ‎Y‎ داخل مجال الأسماء ‎X‎.
  • نوعٌ ‎Z‎ داخل مجال الأسماء ‎X‎ (شكل نسخة الصنف).
  • قيمةٌ ‎Z‎ التي هي خاصيّةٌ من خاصيّات القيمة ‎X‎ (دالة الصنف البانية).

الكتلة الثانية تنشئ معاني الأسماء التاليّة:

  • قيمةٌ ‎Y‎ (من النوع ‎number‎) والتي هي خاصيّةٌ من خاصيّات القيمة ‎X‎.
  • مجالُ أسماءٍ ‎Z‎.
  • قيمةٌ ‎Z‎ والتي هي خاصيّة من خاصيّات القيمة ‎X‎.
  • نوعٌ ‎C‎ في مجال الأسماء ‎X.Z‎.
  • قيمةٌ ‎C‎ والتي هي خاصيّة من خاصيّات القيمة ‎X.Z‎.
  • نوعٌ ‎X‎.

استعمال تصريحات ‎export =‎ أو تصريحات ‎import

لاحظ أنّ تصريحات ‎export‎ وتصريحات ‎import‎ تصدِّر أو تستورد جميع معاني أهدافها.

مصادر