التحقق من الأنواع في ملفات JavaScript في TypeScript

من موسوعة حسوب

مقدمة

أصبحت TypeScript منذ النسخة 2.3 تدعم التحقق من الأنواع والإبلاغ عن الأخطاء في ملفّات ‎.js‎ مع خيار المترجم ‎--checkJs‎.

يمكنك تخطي التحقق من ملفّاتٍ معيّنة عبر إضافة التعليق ‎// @ts-nocheck‎ إليها؛ وفي المقابل يمكنك اختيار ملفّات ‎.js‎ التي تريد التحقق منها عبر إضافة التعليق ‎// @ts-check‎ إليها دون استخدام الخيار ‎‎--checkJs‎‎. يمكنك كذلك تجاهل الأخطاء على أسطرٍ محدَّدة عبر إضافة التعليق ‎// @ts-ignore‎ على نفس السطر. لاحظ أنّه عند ضبط ملفّ ‎tsconfig.json‎، فالتحقق من ملفات JavaScript سيحترم الخيارات الصارمة مثل ‎strictNullChecks‎ ، و ‎noImplicitAny‎، وغيرها. لكن بسبب أنّ التحقق من ملفّات JavaScript ضعيف نسبيًّا فقد يكون دمج الخيارات الصارمة معها مفاجئًا، لذا استخدمها بحذر.

إليك بعض الفروقات بين التحقق من الأنواع في ملفّات ‎.js‎ وفي ملفّات ‎.ts‎:

الفروقات بين التحقق من أنواع ملفّات ‎.js‎ وملفّات ‎.ts

تُستخدَم أنواع JSDoc للحصول على معلومات الأنواع

يُمكن أن تُستنتَج الأنواع في ملفّات ‎.js‎ كما تُستنتَج في ملفّات ‎.ts‎. وعندما لا يمكن استنتاج الأنواع، فيُمكن تحديدها باستخدام JSDoc بنفس الطريقة التي تُستَخدَم فيها حواشي الأنواع (type annotations) في ملفّات ‎.ts، وكما في Typescript، سيُطلق الخيار ‎‎--noImplicitAny‎‎ أخطاءً في الأماكن التي لا يمكن فيها استنتاج النوع. (باستثناء قيم الكائنات الحرفية المفتوحة [open-ended object literals]. انظر أدناه لتفاصيل أكثر).

تُستخدَم حواشي JSDoc التي تُزيِّن تصريحًا لضبط نوعه كما يلي:

/** @type {number} */
var x;

x = 0;      // مسموح
x = false;  // خطأ، لا يمكن تعيين قيمة منطقيّة لمتغيّرٍ نوعُه عددٌ

انظر أدناه لقائمةٍ كاملةٍ تحتوي على أنماط JSDoc المدعومة.

تُستنتَج الخاصيّات من التعيينات في جسم الصنف

لا يوجد طريقة للتصريح عن الخاصيات على الأصناف في النسخة ES2015. تُعيَّن الخاصيات ديناميكيًّا مثلها مثل قيم الكائنات الحرفيّة.

يستنتج المترجم الخاصيات في ملفّات ‎.js‎ من تعيينات الخاصيات داخل جسم الصنف. نوعُ الخاصيات يكون النوعَ المعطى في الدالة البانية، إلا إذا لم يُحدَّد فيها، أو أن النوع المحدد في الدالة البانيّة هو ‎undefined‎ أو ‎null‎. في هذه الحالة، يكون النوع هو اتحاد أنواع جميع القيم في يمين التعيينات. يُفترَض دائمًا بأن الخاصيات المُعرَّفة في الدالة البانيّة موجودة، أما تلك المعرفة في التوابع فقط أو في دوال الوصول (getters) أو الضبط (setters) فتُعَدّ اختيارية.

class C {
    constructor() {
        this.constructorOnly = 0
        this.constructorUnknown = undefined
    }
    method() {
        // خطأ، الخاصيّة
        // constructorOnly
        // عددٌ وليست قيمة منطقيّة
        this.constructorOnly = false

        // مسموح به، الخاصيّة
        // constructorUnknown
        // من النوع
        // string | undefined
        this.constructorUnknown = "plunkbat"

        // مسموح به، لكنّ الخاصيّة
        // methodOnly
        // قد تكون كذلك من النوع
        // undefined
        this.methodOnly = 'ok'
    }
    method2() {
        // مسموح به كذلك
        // أصبح نوع الخاصيّة
        // methodOnly
        // string | boolean | undefined
        this.methodOnly = true
    }
}

إذا لم تُضبَط الخاصيات في جسم الصنف أبدًا (لا في الدالة البانية ولا في غيرها)، فإنّها تُعدّ مجهولة. إذا كان الصنف الخاص بك يحتوي على خاصيّات يُقرأ منها فقط، فأضف تصريحًا في الدالة البانيّة وأضف حاشية له باستخدام JSDoc لتحديد النوع. ولا تحتاج إلى إعطاء قيمة إذا كانت ستُهيّأ لاحقًا:

class C {
    constructor() {
        /** @type {number | undefined} */
        this.prop = undefined;
        /** @type {number | undefined} */
        this.count;
    }
}


let c = new C();
c.prop = 0;          // مسموح
// خطأ، لا يمكن تعيين سلسلة نصيّة لخاصية من النوع
// number|undefined
c.count = "string";

الدوال البانية مكافئة للأصناف

قبل النسخة ES2015، كانت Javascript تعتمد على الدوال البانيّة عوضًا عن الأصناف. يدعم المترجم هذا النمط ويَعُدّ الدوال البانيّة مكافئةً لأصناف ES2015. تعمل قواعد استنتاج أنواع الخاصيات الموصوفة أعلاه بنفس الطريقة:

function C() {
    this.constructorOnly = 0
    this.constructorUnknown = undefined
}
C.prototype.method = function() {
    this.constructorOnly = false // خطأ

    // مسموح، أصبح النوع الآن
    // string | undefined
    this.constructorUnknown = "plunkbat"
}

وحدات CommonJS مدعومة كذلك

في ملفّات ‎.js‎، تفهم Typescript شكل وحدات CommonJS. يُتعرَّف على التعيينات لكل من ‎exports‎ و‎module.exports‎ على أنّها تصريحات تصدير (export declarations). وبالمثل، يُتعرَّف على استدعاءات ‎require‎ على أنّها استيرادات وحدات. على سبيل المثال:

// مكافئ للاستيراد `import module "fs"`
const fs = require("fs");

// مكافئ للتصدير `export function readFile`
module.exports.readFile = function(f) {
    return fs.readFileSync(f);
}

دعم الوحدات في Javascript متسامح أكثر من دعم الوحدات في Typescript. إذ تكون معظم تراكيب التعيينات والتصريحات مدعومة.

الأصناف والدوال وقيم الكائنات الحرفيّة بمثابة مجالات الأسماء (namespaces)

الأصناف مجالاتُ أسماءٍ في ملفّات ‎.js‎. يُمكن استخدام هذه الميّزة لإنشاء أصناف متداخلة، على سبيل المثال:

class C {
}
C.D = class {
}

ولشيفرة ما قبل ES2015، يُمكن استخدامها لمحاكاة التوابع الساكنة:

function Outer() {
  this.y = 2
}
Outer.Inner = function() {
  this.yy = 2
}

يمكن كذلك استخدامها لإنشاء مجالات أسماء بسيطة:

var ns = {}
ns.C = class {
}
ns.func = function() {
}

يُمكن كذلك استعمال أشكال مختلفة:

// IIFE
var ns = (function (n) {
  return n || {};
})();
ns.CONST = 1

// الرجوع افتراضيا للمجال العام
var assign = assign || function() {
  // ضع شيفرتك هنا
}
assign.extra = 1

قيم الكائنات الحرفيّة مفتوحة

في ملفّات ‎.ts‎، تُعطي قيم الكائنات الحرفيّة التي تُهيّئُ تصريح متغيرٍ نوعَها للتصريح. لا يمكن إضافة أية عناصر جديدة لم تُحدَّد في قيمة الكائن الحرفيّة الأصلية. هذه القاعدة غير موجودة في ملفّات ‎.js‎؛ إذ تملك قيم الكائنات الحرفيّة نوعًا مفتوحًا (توقيع فهرس [index signature]) يسمح بإضافة الخاصيّات والبحث عنها ولو لم تكن مُعرَّفة أصلًا. على سبيل المثال:

var obj = { a: 1 };
obj.b = 2;  // مسموح

تتصرّف قيم الكائنات الحرفيّة كما لو امتلكت توقيع الفهرس ‎‎[x:string]: any‎‎ ما يسمح لها بأن تُعامَل على أنّها خرائط (maps) مفتوحة عوضًا عن كونها كائنات مغلقة.

يُمكن تغيير هذا السلوك كما يمكن تغيير سلوكات JavaScript الأخرى عبر تحديد نوع JSDoc للمتغيّر، على سبيل المثال:

/** @type {{a: number}} */
var obj = { a: 1 };

// خطأ، النوع
// {a: number}
// لا يملك الخاصيّةَ
// b
obj.b = 2;

تُعَدّ كل من ‎null‎ و‎undefined‎ ومهيّئات المصفوفات الفارغة على أنّها من النوع ‎any‎ أو ‎any[]

أيّ متغيّرٍ أو معامل أو خاصيّة تُهيّأ بالقيمة ‎null‎ أو القيمة ‎undefined‎ ستملك النوعَ ‎any‎، حتى ولو كان التحقق الصارم من ‎null‎مفعّلًا (strict null checks). وكلّ متغيّر أو معامل أو خاصيّة تهيأ بالقيمة ‎[]‎ ستملك النوع ‎any[]‎، حتى ولو فُعّل التحقق الصارم من ‎null‎. الاستثناء الوحيد هنا هو الخاصيات التي تملك عدّة مُهيِّئات كما هو موضّح أدناه:

function Foo(i = null) {
    if (!i) i = 1;
    var j = undefined;
    j = 2;
    this.l = [];
}
var foo = new Foo();
foo.l.push(foo.i);
foo.l.push("end");

تكون معاملات الدوال اختيارية افتراضيا

تُعدّ جميع معاملات الدوال في ملفّات ‎.js‎ اختياريةً بسبب عدم وجود طريقة لتحديد اختياريةِ المعاملات في نسخ JavaScript التي سبقت النسخة ES2015. يُسمَح باستدعاء الدوال بعدد معاملات أقلّ من عدد المعاملات المصرّح عنها في تعريف الدالة.

لاحظ أنّ استدعاء دالة بعدد زائد من المعاملات خطأ.

على سبيل المثال:

function bar(a, b) {
    console.log(a + " " + b);
}

bar(1);       // مسموح، يعدّ المعامل الثاني اختياريًّا
bar(1, 2);
bar(1, 2, 3); // خطأ، عدد زائد من المعاملات

الدوال ذات حواشي JSDoc مستثناة من هذه القاعدة. استخدم بنية المعاملات الاختيارية في JSDoc للتعبير عن الاختيارية:

/**
 * @param {string} [somebody] – اسمُ شخص ما
 */
function sayHello(somebody) {
    if (!somebody) {
        somebody = 'John Doe';
    }
    console.log('Hello ' + somebody);
}

sayHello();

يُستنتَج تصريح Var-arg (متغيّر-معامل) من طريقة استعمال الكلمة المفتاحية ‎arguments

إذا تواجدت في جسم دالةٍ ما إشارةٌ إلى الكلمة المفتاحية ‎arguments‎ فستُعَد الدالة ذات معامل var-arg (مثل ‎(...arg: any[]) => any‎). استعمل بنية var-arg في JSDoc لتحديد نوع المعاملات:

/** @param {...number} args */
function sum(/* numbers */) {
    var total = 0
    for (var i = 0; i < arguments.length; i++) {
      total += arguments[i]
    }
    return total
}

معاملات الأنواع التي لا تُحدَّد تكون افتراضيًّا النوعَ ‎any

بسبب عدم وجود طريقة لتحديد معاملات الأنواع المعمّمة في JavaScript، فإنّ معاملات الأنواع التي لا تُحدَّد تكون افتراضيًّا النوعَ ‎any‎.

في جملة ‎extends

على سبيل المثال، يُعرَّف ‎React.Component‎ على أنّ له معاملا أنواعٍ، وَهُمَا ‎Props‎ و‎State‎. أمّا في ملفّات ‎.js‎ فما من سبيل لتحديدها في جملة ‎extends‎. ستكون معاملات الأنواع افتراضيًا النوعَ ‎any‎:

import { Component } from "react";

class MyComponent extends Component {
    render() {
        // مسموح به لأنّ
        // this.props
        // من النوع
        // any
        this.props.b;
    }
}

استعمل بنية ‎@augments‎ في JSDoc لتحديد الأنواع صراحةً. على سبيل المثال:

import { Component } from "react";

/**
 * @augments {Component<{a: number}, State>}
 */
class MyComponent extends Component {
    render() {
        // خطأ، الخاصيّة
        // b
        // غير موجودة على
        // {a:number}
        this.props.b;
    }
}

في مراجع JSDoc

عند عدم تحديد نوع المعامل في JSDoc فالنوع يكون افتراضيا النوعَ ‎any‎:

/** @type{Array} */
var x = [];

x.push(1);        // مسموح

// مسموح
// x
// من النوع
// Array<any>
x.push("string");


/** @type{Array.<number>} */
var y = [];

y.push(1);        // مسموح
y.push("string"); // خطأ لا يمكن تعيين سلسلة نصيّة لمصفوفة أعداد

في استدعاءات الدوال

تُستعمَل المعاملات عند استدعاء دالة معمّمة لاستنتاج معاملات الأنواع. تفشل هذه العمليّة أحيانًا في محاولة استنتاج الأنواع، قد يحدث هذا في الأساس بسبب انعدام مصادر الاستنتاج (inference sources)؛ في هذه الحالات تكون معاملات الأنواعِ النوعَ ‎any‎ افتراضيًا. على سبيل المثال:

var p = new Promise((resolve, reject) => { reject() });

p; // Promise<any>;

بنيات JSDoc المدعومة

تُوضّح القائمة أدناه البنيات المدعومة حاليًّا عند استخدام حواشي JSDoc لتوفير معلومات الأنواع في ملفّات JavaScript.

لاحظ أنّ أيّ وسوم غير موجودة وجودًا صريحًا أدناه ليست مدعومةً بعدُ (مثل ‎@async‎).

  • @type
  • @param‎ (أو ‎@arg‎ أو ‎@argument‎)
  • @returns‎ (أو ‎@return‎)
  • @typedef
  • @callback
  • @template
  • @class‎ (أو ‎@constructor‎)
  • @this
  • @extends‎ (أو ‎@augments‎‎)
  • @enum

عادة ما يكون المعنى مطابقًا لمعنى الوسم الموجود في موقع JSDoc أو قد يكون مجموعةً عليا (superset) من معنى الوسم. تصِف الأقسام أدناه الفروقات وتعطي أمثلة على كيفيّة استخدام كل وسم:

@type

يمكنك استعمال الوسم ‎@type‎ مع الإشارة إلى اسم نوعٍ (سواءً أكان نوعًا أوليًّا، أو نوعًا معرّفًا في تصريح TypeScript، أو في وسم ‎@typedef‎ الخاصّ بنظام JSDoc). يمكنك استخدام أي نوعٍ من أنواع TypeScript، ويُمكنك كذلك استخدام معظم أنواع JSDoc:

/**
 * @type {string}
 */
var s;

/** @type {Window} */
var win;

/** @type {PromiseLike<string>} */
var promisedString;

// يمكنك تحديد عنصر
// HTML
// لخاصيّات
// DOM
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = '';

يُمكن تحديد نوع اتحادٍ باستخدام ‎@type‎، على سبيل المثال، يمكن لمتغيّر أن يحمل إمّا سلسلة نصيّة أو قيمة منطقيّة:

/**
 * @type {(string | boolean)}
 */
var sb;

لاحظ أنّ الأقواس اختيارية في أنواع الاتحاد:

/**
 * @type {string | boolean}
 */
var sb;

يمكنك تحديد أنواع المصفوفات باستخدام عدّة أنواع من البنيات (syntaxes):

/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var nds;
/** @type {Array<number>} */
var nas;

يمكنك كذلك تحديد أنواع قيم الكائنات الحرفيّة. على سبيل المثال، سيستعمل كائنٌ ذو الخاصيّة النصيّة ‎a‎ والخاصيّة العدديّة ‎b‎ البنية التالية:

/** @type {{ a: string, b: number }} */
var var9;

يمكنك تحديد الكائنات التي تشبه الخرائط (map-like) وتلك التي تشبه المصفوفات (array-like) باستخدام تواقيع فهرس نصيّة وعدديّة، يُمكنك استخدام كل من بنية JSDoc أو بنية Typescript:

/**
 * كائن شبيه بالخرائط يربط خاصيّات نصيّة بأعداد
 *
 * @type {Object.<string, number>}
 */
var stringToNumber;

/** @type {Object.<number, object>} */
var arrayLike;

النوعان السابقان مكافئان للنوعين ‎{ [x: string]: number }‎ و‎{ [x: number]: any }‎ في TypeScript. وسيفهم المترجم كلا البنيتين.

يُمكنك تحديد أنواع الدوال باستخدام إمّا بنية لغة Typescript أو بنية لغة Closure:

/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} Typescript syntax */
var sbn2;

أو يُمكنك كذلك استخدام النوع ‎Function‎ غير المحدَّدِ الذي يشير إلى أنّ الكائن دالةٌ وحسب:

/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;

هناك أنواع Closure أخرى تعمل كذلك:

/**
 * @type {*} - يُمكن أن يكون أي نوع كيفما كان ('any')
 */
var star;
/**
 * @type {?} - نوع مجهول (مكافئ له 'any')
 */
var question;

التحويلات (Casts)

استعارت Typescript بنية التحويلات من Closure. يسمح لك هذا بتحويل الأنواع إلى أنواع أخرى عبر إضافة وسم ‎@type‎ قبل أي تعبير محصور بين قوسين (parenthesized):

/**
 * @type {number | string}
 */
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString)

أنواع الاستيراد (Import types)

يمكنك كذلك استيراد تصريحاتٍ من ملفّات أخرى باستخدام أنواع الاستيراد. هذه البنية خاصّة بلغة Typescript وتختلف عن معيار JSDoc:

/**
 * @param p { import("./a").Pet }
 */
function walk(p) {
    console.log(`Walking ${p.name}...`);
}

يُمكن كذلك استخدام أنواع الاستيراد في تصريحات أسماء الأنواع البديلة (type alias declarations):

/**
 * @typedef Pet { import("./a").Pet }
 */

/**
 * @type {Pet}
 */
var myPet;
myPet.name;

يمكن استخدام أنواع الاستيراد للحصول على نوع قيمةٍ من وحدة إذا لم تعرف النوع، أو إن كان اسم النوع طويلًا ولم ترغب بكتابته:

/**
 * @type {typeof import("./a").x }
 */
var x = require("./a").x;

@param‎ و‎@returns

يستعمل الوسم ‎@param‎ نفس بنية الأنواع التي يستخدمها الوسم ‎@type‎، لكنّه يُضيف اسم مُعامل. يُمكن كذلك التصريح عن المعامل كمعاملٍ اختياري عبر إحاطة الاسم بأقواس مربّعة ([]):

// يمكن التصريح عن المعاملات بعدة أشكال من البنيات
/**
 * @param {string}  p1 – معامل نصيّ.
 * @param {string=} p2 – معامل اختياري (Closure)
 * @param {string} [p3] – معامل اختياريّ آخر (JSDoc).
 * @param {string} [p4="test"] – معامل اختياري ذو قيمة افتراضية
 * @return {string} هذه هي النتيجة
 */
function stringsStringStrings(p1, p2, p3, p4){
  // TODO
}

وبالمثل، يمكن تحديد النوع المعاد لدالة كما يلي:

/**
 * @return {PromiseLike<string>}
 */
function ps(){}

/**
 * @returns {{ a: string, b: number }} - سواء '@returns' أو '@return'
 */
function ab(){}

@typedef‎ و‎@callback‎ و‎@param

يمكن استخدام ‎@typedef‎ لتعريف أنواع معقّدة. والبنية تعمل بشكل مشابه للوسم ‎@param‎:

/**
 * @typedef {Object} SpecialType – هذا ينشئ نوعًا جديدًا باسم 'SpecialType'
 * @property {string} prop1 – خاصية نصيّة في النوع الجديد
 * @property {number} prop2 – خاصيّة عدديّة في النوع الجديد
 * @property {number=} prop3 – خاصيّة عدديّة اختيارية في النوع الجديد
 * @prop {number} [prop4] - خاصيّة عدديّة اختيارية في النوع الجديد
 * @prop {number} [prop5=42] - خاصيّة عدديّة اختيارية في النوع الجديد مع قيمة افتراضية
 */

// هنا نستعمل النوع الجديد الذي أنشأناه للتو
/** @type {SpecialType} */
var specialTypeObject;

يمكنك استخدام إمّا ‎object‎ أو ‎Object‎ على السطر الأول:

/**
 * @typedef {object} SpecialType1 - هذا ينشئ نوعًا جديدًا باسم 'SpecialType1'
 * @property {string} prop1 - خاصية نصيّة في النوع الجديد
 * @property {number} prop2 - خاصيّة عدديّة في النوع الجديد
 * @property {number=} prop3 - خاصيّة عدديّة اختيارية في النوع الجديد
 */
/** @type {SpecialType1} */
var specialTypeObject1;

يسمح ‎@param‎ باستخدام بنية مشابهة لتحديد نوعٍ لاستخدامه مرّة واحدة. لاحظ أنّه ينبغي على الخاصيات المتداخلة أن تُسبَق باسم المعامل:

/**
 * الشكل هو نفسه شكل النوع
 * SpecialType
 * أعلاه
 *
 * @param {Object} options
 * @param {string} options.prop1
 * @param {number} options.prop2
 * @param {number=} options.prop3
 * @param {number} [options.prop4]
 * @param {number} [options.prop5=42]
 */
function special(options) {
  return (options.prop4 || 1001) + options.prop5;
}

الوسم ‎@callback‎ مشابه للوسم ‎@typedef‎، لكنّه يحدّد نوع دالةٍ عوضًا عن نوع كائنٍ:

/**
 * @callback Predicate
 * @param {string} data
 * @param {number} [index]
 * @returns {boolean}
 */
/** @type {Predicate} */
const ok = s => !(s.length % 2);

ويُمكن طبعًا التصريح عن أيٍّ من هذه الأنواع باستخدام بنية Typescript في سطر واحد بالوسم ‎@typedef‎:

/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */

@template

يمكنك التصريح عن أنواعٍ معمّمة (generic types) بالوسم ‎@template‎:

/**
 * @template T
 * @param {T} p1 – معامل معمّم ينتقل إلى النوع المعاد
 * @return {T}
 */
function id(x){ return x }

استعمل فاصلة أو عدّة وسوم للتصريح عن عدّة معاملاتِ أنواعٍ:

/**
 * @template T,U,V
 * @template W,X
 */

يمكنك كذلك تحديد قيد نوعٍ (type constraint) قبل اسم معامل النوع. معامل النوع الأول في القائمة هو وحده من يُقيَّد:

/**
 * يجب على
 * K
 * أن يكون سلسلة نصيّة أو قيمة سلسلة نصيّة حرفيّة
 * @template {string} K
 *
 * يجب أن يحتوي على تابع باسم
 * "serious"
 * @template {{ serious(): string }} Seriousalizable
 * @param {K} key
 * @param {Seriousalizable} object
 */
function seriousalize(key, object) {
  // ????
}

@constructor

يستنتج المترجم الدوال البانيّة بناءً على التعيينات للخاصيّة ‎this‎، لكن يمكنك جعل التحقّق أكثر صرامةً ويُمكنك كذلك تحسين الاقتراحات بإضافة الوسم ‎@constructor‎:

/**
 * @constructor
 * @param {number} data
 */
function C(data) {
  this.size = 0;
  this.initialize(data); // ينبغي أن يُطلَق خطأ لأنّ المهيئ يتوقَّع سلسلةً نصيّة
}
/**
 * @param {string} s
 */
C.prototype.initialize = function (s) {
  this.size = s.length
}

var c = new C(0);

// يجب استدعاء
// C
// فقط بالكلمة المفتاحية
// new
var result = C(1);

باستخدام الوسم ‎@constructor‎، سيُتحقَّق من ‎this‎ داخل الدالة البانيّة ‎C‎، لذا ستحصل على اقتراحات للتابع ‎initialize‎ وستحصل على خطأ إذا مرّرت لها عددًا. ستحصل على خطأ كذلك إذا استدعيت ‎C‎ عوضًا عن بنائها (بالكلمة المفتاحيّة ‎new‎).

لكن هذا يعني للأسف أنّ الدوال البانيّة القابلة للاستدعاء لا تستطيع استخدام الوسم ‎@constructor‎ لأنّه يمنع الاستدعاء.

@this

يمكن عادةً للمترجم استنتاج نوعِ ‎this‎ عندما يكون هناك سياق يساعد على ذلك. لكن عندما لا يستطيع المترجم استنتاج نوع ‎this‎، فتستطيع تحديده بالوسم ‎@this‎:

/**
 * @this {HTMLElement}
 * @param {*} e
 */
function callbackForLater(e) {
    this.clientHeight = parseInt(e) // مسموح به
}

@extends

عندما تُوسِّع أصناف JavaScript صنفًا أساسًا مُعمَّمًا (generic base class)، فلا يوجد مكان لتحديد معامل النوع. يوفِّر الوسم ‎@extends‎ مكانًا لمعامل الأنواع هذا:

/**
 * @template T
 * @extends {Set<T>}
 */
class SortableSet extends Set {
  // ...
}

لاحظ أنّ الوسم ‎@extends‎ يعمل فقط مع الأصناف. لا توجد طريقة حاليًّا لتوسيع صنفٍ من طرف دالّةٍ بانيّة.

@enum

يسمح الوسم ‎@enum‎ بإنشاء قيمة كائن حرفيّة عناصرها من نوعٍ واحدٍ محدّد. ولا تسمح لعناصر أخرى بأن تكون في الكائن على النقيض من معظم قيم الكائنات الحرفيّة الموجودة في JavaScript.

/** @enum {number} */
const JSDocState = {
  BeginningOfLine: 0,
  SawAsterisk: 1,
  SavingComments: 2,
}

لاحظ أنّ ‎@enum‎ مختلفة عن الثوابت المتعدّدة ‎enum‎ في Typescript، إذ أنّ ‎@enum‎ أبسط وأكثر وضوحًا. وعلى النقيض من الثوابت المتعدّدة في Typescript، فالوسم ‎@enum‎ قادرٌ على حمل أي نوع:

/** @enum {function(number): number} */
const Math = {
  add1: n => n + 1,
  id: n => -n,
  sub1: n => n - 1,
}

المزيد من الأمثلة

var someObj = {
  /**
   * @param {string} param1 – تعمل الحواشي كذلك داخل تعيينات الخاصيات
   */
  x: function(param1){}
};

/**
 * وكذلك في تعيينات المتغيّرات
 * @return {Window}
 */
let someFunc = function(){};

/**
 * وتوابع الأصناف كذلك
 * @param {string} greeting
 */
Foo.prototype.sayHi = (greeting) => console.log("Hi!");

/**
 * وفي تعابير الدوال السهمية
 * @param {number} x – دالّة جداء
 */
let myArrow = x => x * x;

/**
 * ما يعني أنّها تعمل كذلك في مكونات الدوال عديمة الحالة في
 * JSX
 * @param {{a: string, b: number}} test – معاملٌ ما
 */
var sfc = (test) => <div>{test.a.charAt(0)}</div>;

/**
 * يمكن لمعاملٍ أن يكون دالّة بانيّة باستخدام بنية لغة
 * Closure
 *
 * @param {{new(...args: any[]): object}} C – الصنف المرغوب تسجيله
 */
function registerClass(C) {}

/**
 * معامل بقيّة
 * ('rest' arg)
 * يُمثّل مصفوفة من السلاسل النصيّة، ويُعامَل كالنوع
 * 'any'
 *
 * @param {...string} p1
 */
function fn10(p1){}

/**
 * معامل بقيّة
 * ('rest' arg)
 * يُمثّل مصفوفة من السلاسل النصيّة، ويُعامَل كالنوع
 * 'any'
 *
 * @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
 */
function fn9(p1) {
  return p1.join();
}

الأنماط غير المدعومة

إنشاء مراجع تشير إلى كائنات في مكان القيمة كأنواعٍ لا يعمل إلا إذا كان الكائن يُنشئ كائنًا كذلك، مثل دالة بانيّة:

function aNormalFunction() {

}
/**
 * @type {aNormalFunction}
 */
var wrong; // خطأ
/**
 * استعمل
 * 'typeof'
 * كبديل
 * @type {typeof aNormalFunction}
 */
var right; // صحيح

إلحاق المحرف ‎=‎ إلى نوع خاصيّةٍ في قيمة كائن حرفيّةٍ لا يُحدِّد أنّ الخاصية اختيارية:

/**
 * @type {{ a: string, b: number= }}
 */
var wrong; // خطأ
/**
 * استعمل المحرف
 * ?
 * كبديل
 * @type {{ a: string, b?: number }}
 */
var right; // صحيح

لا معنى للأنواع التي تقبل القيمة ‎null‎ إلّا عند تفعيل الخيار ‎strictNullChecks‎:

/**
 * @type {?number}
 * عند تفعيل الخيار
 * strictNullChecks: true -- number | null
 * عند تعطيله
 * strictNullChecks: off  -- number
 */
var nullable;

لا معنى للأنواع التي لا تقبل القيمة ‎null‎ وتُعامَل كما تُعامل أنواعها الأصليّة:

/**
 * @type {!number}
 *
 * سيحمل النوع
 * number
 * فقط
 */
var normal;

على النقيض من نظام أنواع JSDoc، فلغة Typescript تسمح لك بتعليم (mark) الأنواع على أنّها تحمل القيمة ‎null‎ أو لا. لا يوجد طريقة لإعلان أنّ النوع لا يقبل القيمة ‎null‎. إذا كان الخيار ‎strictNullChecks‎ مفعّلًا، فعندها لا يمكن للنوع ‎number‎ أن يحمل القيمة ‎null‎. إذا كان الخيار معطّلًا، فعندئذٍ سيقبل النوعُ ‎number‎ القيمةَ ‎null‎.

مصادر