JSX في TypeScript

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

مقدمة

JSX بنيةٌ (syntax) مشابهةٌ للغة XML يُمكن تضمينها وتحويلها إلى شيفرة JavaScript صالحة، لكن آلية عملية التحويل تختلف من تطبيق (implementation) إلى آخر. اشتهرت JSX باستخدامها مع مكتبة React، لكنها تملك تطبيقات أخرى غير React. تدعم لغة TypeScript تضمين JSX، وتدقيق الأنواع فيها، وكذا ترجمة JSX إلى JavaScript مباشرةً.

ملاحظة حول المصطلحات: نستخدم في هذه الصّفحة كلمة "صِفَة" للإشارة إلى الكلمة الإنجليزيّة "attribute" وكلمة "خاصيّة" للإشارة إلى كلمة "property"، كلاهما يُترجَم إلى كلمة "خاصيّة" ولهما نفس المعنى في معظم الأحيان، إلّا أنّنا نُشير مثلًا إلى خاصيّات HTML بعبارة "HTML attribute" ونشير إلى خاصيّات كائنٍ في JavaScript بعبارة "object property". وفي JSX نستخدم صفات JSX (أي JSX attributes)، وخاصيّات React (أي React properties).

بداية

تحتاج إلى أمرين لاستخدام JSX:

  1. تسميّة ملفّاتك بامتداد ‎.tsx‎.
  2. تفعيل خيار ‎jsx‎.

تُوفِّر TypeScript ثلاثة أوضاع (JSX modes) للتعامل مع JSX وهي: ‎preserve‎، و‎react‎، و‎react-native‎.

تؤثّر هذه الأوضاع الثلاثة على مرحلة الإخراج فقط (أي أنّ آليّة التحقّق من الأنواع لا تتأثّر).

وضع ‎preserve‎ يُبقي شيفرة JSX كجزء من المُخرَج لينتقل إلى مرحلة تحويل أخرى (التحويل بمُترجم Babel مثلًا)، إضافةً أنّ الملفّ المُخرَج سيكون ذا الامتداد ‎.jsx‎.

وضع ‎react‎ يُخرِج ‎React.createElement‎ (انظر صفحة مقدّمة إلى JSX في توثيق React)، ولا يحتاج إلى مرحلة تحويل JSX أخرى قبل الاستعمال، وسيمتلك الملفّ المُخرَج الامتداد ‎.js‎.

وضع ‎react-native‎ مكافئٌ للوضع ‎preserve‎، أي أنّه يُبقي على جميع شيفرات JSX‎، لكنّ الملفّ المُخرَج سيكون ذا الامتداد ‎.js‎.

الوضع المُدخَل (Input) المُخرَج (Output) امتداد الملفّ المُخرَج
preserve <div /> <div /> .jsx
react <div /> React.createElement("div") .js
react-native <div /> <div /> .js

يُمكنك تحديد هذا الوضع باستخدام خيار سطر الأوامر ‎--jsx‎ أو الخيار المكافئ في ملفّ ‎tsconfig.json‎ الخاصّ بك.

ملاحظة: المُعرِّف ‎React‎ مكتوب كما هو في شيفرة TypeScript الداخلية المسؤولة عن JSX، لذا عليك توفير React بحرف R كبيرة.

العامل ‎as

تذكّر كيف كنّا نكتب إثباتات الأنواع (type assertion):

var foo = <foo>bar;

يُثبت هذا بأنّ ‎bar‎ من النوع ‎foo‎. ولأن TypeScript تستخدم كذلك أقواس الزاوية لإثباتات الأنواع، فمزجها مع بنية JSX سيخلق بعض المشاكل. لذا فإنّ TypeScript تمنع إثباتات الأنواع التي تستخدم أقواس الزاوية في ملفّات ‎.tsx‎.

ولأنّنا لا نستطيع استخدام البنية في المثال أعلاه في ملفّات ‎.tsx‎، فسيتوجّب علينا استخدام العامل البديل ‎as‎. يُمكن إعادة كتابة المثال بسهولة مع العامل ‎as‎:

var foo = bar as foo;

العامل ‎as‎ متاح في كل من ملفّات ‎.ts‎ وملفّات ‎.tsx‎، ويسلك نفس سلوك إثباتات الأنواع ذات أقواس الزاوية.

التحقّق من الأنواع

لفهم آلية التحقق من الأنواع في JSX، عليك أولًا فهم الفرق بين العناصر الجوهريّة (intrinsic elements) والعناصر التي تعتمد على القيم (value-based elements). لنفترض أنّ لدينا تعبير JSX كالتالي: ‎<expr />‎، يُمكن هنا للكائن ‎expr‎ أن يُشير إلى شيءٍ جوهريّ في البيئة (مثل ‎div‎ أو ‎span‎ في بيئة DOM مثلًا)، أو يُمكن للكائن ‎expr‎ أن يُشير إلى مكوّن خاصّ أنشأتَه بصفتك المبرمج. هذا مُهمّ لسببين:

  1. في React، تُخرَج العناصر الجوهريّة كسلاسل نصيّة (‎‎React.createElement("div")‎‎‎)، أمّا المكونات التي أنشأتها فلا (‎‎React.createElement(MyComponent)‎‎).
  2. يجب البحث عن أنواع الصّفات (attributes) التي تُمرَّر في عنصر JSX بشكل مغاير. من المفترض أن تُعرَف صفات العناصر الجوهريّة جوهريًّا، أمّا المكونات فالأغلب أنّها تُحدِّد مجموعة صفاتها الخاصّة.

تستخدم TypeScript نفس القاعدة التي تتّبعها React للتمييز بين هاذين النوعين من العناصر. دائمًا ما تبدأ العناصر الجوهريّة بحرف صغيرة (lowercase letter)، وتبدأ العناصر التي تعتمد على القيم بحرف كبيرة (uppercase letter) دائمًا.

العناصر الجوهريّة (Intrinsic elements)

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

declare namespace JSX {
    interface IntrinsicElements {
        foo: any
    }
}

<foo />; // مسموح
<bar />; // خطأ

في المثال أعلاه سيعمل ‎<foo />‎ بنجاح لكنّ ‎<bar />‎ سيُنتِج خطأ لأنّه لم يُحدَّد على الواجهة ‎JSX.IntrinsicElements‎.

ملاحظة

يُمكنك كذلك تحديد مُفهرس نصيّ (string indexer) يلتقط جميع الأسماء على الواجهة ‎JSX.IntrinsicElements‎ كما يلي:

declare namespace JSX {
   interface IntrinsicElements {
       [elemName: string]: any;
   }
}

العناصر المعتمدة على القيم (Value-based elements)

يُبحَث عن العناصر المعتمدة على القيم بالمعرِّفات (identifiers) الموجودة في المجال ببساطة:

import MyComponent from "./myComponent";

<MyComponent />; // مسموح به لأن المُعرِّف موجود في المجال (انظر الاستيراد)
<SomeOtherComponent />; // خطأ

هناك طريقتان لتعريف عنصر معتمد على القيم:

  1. مكون وظيفيّ عديم الحالة (Stateless Functional Component) الذي يُعرَف بالاختصار SFC.
  2. مكّونُ صنف (Class Component).

لا يُمكن التمييز بين هاذين النوعين من العناصر المعتمدة على القيم في تعابير JSX، لذا تُحاول TypeScript أولًا تقرير (resolve) التعبير كمكوّن وظيفيّ عديم الحالة باستخدام تقرير الأحمال الزائدة (overload). إذا نجحت العمليّة، فستُنهي TypeScript تقرير التعبير إلى التصريح عنه. وإذا فشل التقرير إلى مكوّن SFC، فستحاول TypeScript حينئذ تقريره على أنّه مكوّن صنف. وإذا فشل ذلك فسيُطلَق خطأ.

المكونات الوظيفيّة عديمة الحالة

تُعرَّف المكونات الوظيفيّة عديمة الحالة كدوال JavaScript، حيث الكائنُ ‎props‎ هو المعامل الأول للدالة. تقضي TypeScript بوجوب قابليّة تعيين نوع الدالة المعاد (return type) إلى ‎JSX.Element‎.

interface FooProp {
  name: string;
  X: number;
  Y: number;
}

declare function AnotherComponent(prop: {name: string});
function ComponentFoo(prop: FooProp) {
  return <AnotherComponent name={prop.name} />;
}

const Button = (prop: {value: string}, context: { color: string }) => <button>

يُمكن استخدام الأحمال الزائدة (function overloads) هنا لأن مكوّنات SFC مجرّد دوال JavaScript عاديّة:

interface ClickableProps {
  children: JSX.Element[] | JSX.Element
}

interface HomeProps extends ClickableProps {
  home: JSX.Element;
}

interface SideProps extends ClickableProps {
  side: JSX.Element | string;
}

function MainButton(prop: HomeProps): JSX.Element;
function MainButton(prop: SideProps): JSX.Element {
  ...
}

مكوّنات الأصناف

يُمكن تعريف نوع مكوّن صنف. لكن للقيام بذلك علينا أولًا فهم مصطلحين جديدين: نوع صنف العنصر (element class type) ونوع نسخة العنصر (element instance type).

لنفترض أنّ لدينا المكوّن ‎<Expr />‎، نوع صنف العنصر هو نوعُ ‎Expr‎. لذا في المثال أعلاه، إذا كان ‎MyComponent‎ صنفًا في نسخة ES6، فنوع الصنف هو دالته البانيّة (constructor) وعناصره الساكنة (statics). وإن كان ‎MyComponent‎ دالةً مولِّدةً (factory function) فنوع الصنف سيكون هو هذه الدالة.

حالما يُحصَل على نوع الصنف، فسيُحدَّد نوع النسخة عبر اتحاد الأنواع المُعادة الخاصّة بتوقيعات البناء (construct signatures) أو توقيعات الاستدعاء (call signatures) الخاصّة بنوع الصنف حسب وجود أي منهما. لذا في حالة صنف ES6، فنوع النسخة سيكون نوع نسخةٍ من هذا الصنف، وفي حالة دالة مولِّدة، فالنوع هو القيمة المُعادة من طرف الدالة.

class MyComponent {
  render() {}
}

// استخدم توقيع بناء
var myComponent = new MyComponent();

// نوع صنف العنصر => MyComponent
// نوع نسخة العنصر => { render: () => void }

function MyFactoryFunction() {
  return {
    render: () => {
    }
  }
}

// استخدم توقيع استدعاء
var myComponent = MyFactoryFunction();

// نوع صنف العنصر => MyFactoryFunction
// نوع نسخة العنصر => { render: () => void }

يجب على نوع نسخة العنصر أن يكون قابلًا للتعيين للنوع ‎JSX.ElementClass‎ وإلّا سيُطلِق المترجم خطأ. افتراضيًا يكون ‎JSX.ElementClass‎ هو ‎{}‎، لكن يُمكن تنميّته لتقييد استخدام JSX لتعمل فقط مع الأنواع التي تُوافِق الواجهة الصحيحة:

declare namespace JSX {
  interface ElementClass {
    render: any;
  }
}

class MyComponent {
  render() {}
}
function MyFactoryFunction() {
  return { render: () => {} }
}

<MyComponent />; // مسموح
<MyFactoryFunction />; // مسموح

class NotAValidComponent {}
function NotAValidFactoryFunction() {
  return {};
}

<NotAValidComponent />; // خطأ
<NotAValidFactoryFunction />; // خطأ

التحقق من أنواع الصفات (Attribute type checking)

الخطوة الأولى للتحقق من أنواع الصفات هي تحديد نوع صفات العنصر (element attributes type). هذا مختلف قليلًا بين العناصر الجوهريّة والعناصر المعتمدة على القيم.

للعناصر الجوهريّة، هذا النوع هو نوع الخاصيّة على واجهة ‎JSX.IntrinsicElements‎:

declare namespace JSX {
  interface IntrinsicElements {
    foo: { bar?: boolean }
  }
}

// نوع صفات العنصر للعنصر
// 'foo'
// هو
// '{bar?: boolean}'
<foo bar />;

بالنسبة للعناصر المعتمدة على القيم، فالأمر أعقد. يُحدَّد النوعُ حسب نوع خاصيةٍ على نوع نسخة العنصر الذي حُدِّد مسبقًا. تُحدَّد الخاصيّة التي سيُعتمد عليها من طرف الواجهة ‎JSX.ElementAttributesProperty‎ التي يجب التصريح عنها بخاصيّة واحدة، وسيُستعمَل بعد ذلك اسم هذه الخاصيّة. في نسخة TypeScript 2.8 وما تلاها، إذا لم تُحدَّد الواجهة ‎JSX.ElementAttributesProperty‎، فسيُستخدَم نوع المعامل الأول الخاص بالدالة البانية لعنصر الصنف أو استدعاء SFC.

declare namespace JSX {
  interface ElementAttributesProperty {
    props; // تحديد اسم الخاصية المرغوب استخدامها
  }
}

class MyComponent {
  // تحديد الخاصية على نوع نسخة العنصر
  props: {
    foo?: string;
  }
}

// نوع صفات العنصر للمكوّن
// 'MyComponent'
// هو
// '{foo?: string}'
<MyComponent foo="bar" />

يُستخدم نوع صفة العنصر للتحقق من أنواع الصفات في JSX، يدعم هذا كلًّا من الخاصيات المطلوبة والاختيارية:

declare namespace JSX {
  interface IntrinsicElements {
    foo: { requiredProp: string; optionalProp?: number }
  }
}

<foo requiredProp="bar" />; // مسموح
<foo requiredProp="bar" optionalProp={0} />; // مسموح

// خطأ، الخاصيّة
// requiredProp
// مطلوبة
<foo />;

// خطأ، يجب على الخاصيّة
// requiredProp
// أن تكون سلسلةً نصيّة
<foo requiredProp={0} />;

// خطأ، الخاصيّة
// unknownProp
// غير موجودة
<foo requiredProp="bar" unknownProp />;

// مسموح لأنّ
// 'some-unknown-prop'
// ليس مُعرِّفًا صالحًا
<foo requiredProp="bar" some-unknown-prop />;

ملاحظة: إذا لم يكن اسمُ صفةٍ مُعرِّفَ JavaScript صالح (مثل الصّفة ‎data-*‎)، فلا يُعدّ عدم وجوده في نوع صفات العنصر خطأً.

إضافةً إلى ما سبق، يُمكن استخدام الواجهة ‎JSX.IntrinsicAttributes‎ لتحديد خاصيّات إضافيّة تُستخدَم من طرف إطار عمل JSX ولا تستخدم عامّةً من طرف خاصيات المكونات أو معاملاتها (مثل الخاصية ‎key‎ في React). للتخصّص أكثر، يُمكن كذلك استخدام النوع المعمّم ‎JSX.IntrinsicClassAttributes<T>‎‎ لتحديد نفس شكل الصفات الإضافيّة لمكونات الأصناف فقط (وليس لمكونات SFC). في هذا النوع يشير المعامل المعمَّم (generic parameter) إلى نوع نسخة الصنف. يُستخدَم هذا في React للسماح بالصفة ‎ref‎ من النوع ‎Ref<T>‎‎. عمومًا يجب على جميع الخاصيات على هذه الواجهات أن تكون اختيارية، إلا إذا كنت ترغب من مستخدمي إطار العمل الخاص بك المتعمد على JSX بأن يُوفّروا صفةً على كل وسم (tag).

عامل النّشر (spread operator) يعمل كذلك:

var props = { requiredProp: "bar" };
<foo {...props} />; // مسموح

var badProps = {};
<foo {...badProps} />; // خطأ

التحقق من أنواع الأبناء (Children Type Checking)

أُضيف التحقق من أنواع خاصيّة الأبناء ‎children‎ في النسخة 2.3 من TypeScript. الخاصيّة ‎children‎ خاصيّةٌ خاصّةٌ في نوع صفات العنصر حيث تؤخذ تعابير JSX الأبناء (child JSXExpressions) لتُدخَل إلى الصفات. كما تعتمد TypeScript على ‎JSX.ElementAttributesProperty‎ لتحديد اسم الخاصيّات ‎props‎، فإنّها تعتمد كذلك على ‎JSX.ElementChildrenAttribute‎ لتحديد اسم الخاصيّة ‎children‎ داخل هذه الخاصيات. يجب التصريح عن الواجهة ‎JSX.ElementChildrenAttribute‎ بخاصيّة واحدة فقط:

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: {};  // تحديد اسم خاصيّة الأبناء المرغوب استخدامه
  }
}
<div>
  <h1>Hello</h1>
</div>;

<div>
  <h1>Hello</h1>
  World
</div>;

const CustomComp = (props) => <div>props.children</div>
<CustomComp>
  <div>Hello World</div>
  {"هذا مجرد تعبير جافاسكربت عادي‎" + 1000}
</CustomComp>

يُمكنك تحديد نوع الأبناء كأي صفة أخرى. سيُغطّي هذا على النوع الافتراضي الذي يأتي من تنويعات React إن كنت تستخدمها مثلًا:

interface PropsType {
  children: JSX.Element
  name: string
}

class Component extends React.Component<PropsType, {}> {
  render() {
    return (
      <h2>
        {this.props.children}
      </h2>
    )
  }
}

// مسموح
<Component>
  <h1>Hello World</h1>
</Component>

// خطأ، الأبناء
// children
// من النوع
// JSX.Element
// وليس مصفوفة من
// JSX.Element

<Component>
  <h1>Hello World</h1>
  <h2>Hello World</h2>
</Component>

// خطأ، الأبناء
// children
// من النوع
// JSX.Element
// وليس مصفوفة من
// JSX.Element
// أو سلسلةً نصيّة
<Component>
  <h1>Hello</h1>
  World
</Component>

نوع نتيجة JSX

نوع نتيجة تعبير JSX يكون افتراضيًّا النوعَ ‎any‎. يُمكنك تخصيص النوع عبر تحديد الواجهة ‎JSX.Element‎. لكن لا يمكن الحصول على معلومات النوع حول عنصر، أو صفات أو أبناء JSX من هذه الواجهة.

تضمين التعابير (Embedding Expressions)

تسمح لك JSX بتضمين التعابير بين الوسوم عبر إحاطة التعابير بقوسين معقوفين (‎{ }‎).

var a = <div>
  {["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>

ستطلق الشيفرة أعلاه خطأ لأن تقسيم سلسلة نصيّة على عدد غير مسموح به. سيكون المخرج كما يلي عند استخدام الخيار ‎preserve‎:

var a = <div>
  {["foo", "bar"].map(function (i) { return <span>{i / 2}</span>; })}
</div>

استخدام React

لاستخدام JSX مع React يجب عليك استخدام تنويعات React. هذه التنويعات تُعرّف مجال الأسماء ‎JSX‎ لتُستخدَم JSX مع React كما ينبغي:

/// <reference path="react.d.ts" />

interface Props {
  foo: string;
}

class MyComponent extends React.Component<Props, {}> {
  render() {
    return <span>{this.props.foo}</span>
  }
}

<MyComponent foo="bar" />; // مسموح
<MyComponent foo={0} />; // خطأ

الدوال المولِّدة (Factory Functions)

يُمكن ضبط الدالّة المُولِّدة التي سيُعتمَد عليها من طرف خيار المترجم ‎jsx: react‎. يُمكن ضبطها إمّا عبر خيار سطر الأوامر ‎jsxFactory‎، أو عن طريق تعليمة التعليق ‎@jsx‎ على السطر لضبطها في كل ملفّ. على سبيل المثال، إن ضبطتَ ‎jsxFactory‎ إلى ‎createElement‎، فسيُخرِج التعبير ‎<div />‎ الاستدعاءَ ‎createElement("div")‎ عوضًا عن الاستدعاء ‎React.createElement("div")‎.

يُمكن استعمال طريقة تعليمة التعليق كما يلي (في TypeScript 2.8):

import preact = require("preact");
/* @jsx preact.h */
const x = <div />;

هذا يولد الشيفرة التالية:

const preact = require("preact");
const x = preact.h("div", null);

ستؤثّر الدالة المولِّدة المُختارة على المكان الذي سيُبحث فيه عن مجال الأسماء ‎JSX‎ (للحصول على معلومات التحقّق من الأنواع) قبل العودة إلى المجال العام. إذا كانت الدالة المولِّدة هي ‎React.createElement‎ (أي الدالة الافتراضية)، فسيتحقّق المترجم من وجود مجال الأسماء ‎React.JSX‎ قبل التحقق من وجود ‎JSX‎ عام. إذا كانت الدالة المولِّدة معرّفة في ‎h‎، فسيتُحقَّق من ‎h.JSX‎ قبل ‎JSX‎ عام.

مصادر