الأنواع المتقدمة في TypeScript

من موسوعة حسوب
< TypeScript
مراجعة 12:32، 25 سبتمبر 2018 بواسطة عبد-الهادي-الديوري (نقاش | مساهمات) (توحيد المصطلحات)
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)
اذهب إلى التنقل اذهب إلى البحث

أنواع التقاطع (Intersection Types)

يجمعُ نوعُ تقاطعٍ عدّة أنواع في نوع واحد. يسمح هذا بجمع الأنواع الموجودة في نوع واحد يملك جميع الميّزات التي تحتاج إليها. مثلًا، النوعُ ‎Person & Serializable & Loggable‎ هو من النّوع ‎Person‎ والنّوع ‎Serializable‎ والنّوع ‎Serializable‎، كلّها مجموعة في نفس النّوع. هذا يعني أنّ كائنًا ما من هذا النوع سيحتوي على جميع عناصر الأنواع الثلاثة.

تُستعمل أنواع التّقاطع عادةً في المخاليط (mixins) والمبادئ الأخرى التي قد تكون غريبة على البرمجة كائنيّة التوجه العاديّة (وهي متواجدة كثيرًا في لغة JavaScript). إليك مثالًا يعرض كيفيّة إنشاء مخلوط:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

أنواع الاتحاد (Union Types)

أنواع الاتحاد مرتبطة بأنواع التقاطع، لكنها تعمل بآلية مختلفة تمامًا.

قد تتوقّع أحيانًا دالّةٌ في مكتبة ما مُعاملًا قد يكون إمّا من النوع ‎number‎ أو من النوع ‎string‎. مثلًا انظر الدالة التالية التي تأخذ سلسلةً نصيّةً وتُضيف إليها حاشيّة من المسافات (padding) على يسارها، إن كانت قيمة المعامل ‎padding‎ سلسلةً نصيّة، فستُضاف قيمته إلى يسار القيمة ‎value‎، أمّا إن كانت قيمة ‎padding‎ عددًا فسيُضاف هذا العدد من المسافات إلى يسار القيمة:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

// مثال على استخدام الدالة
padLeft("Hello world", 4); // "    Hello world"

المشكلة هنا أنّ نوعَ المعاملِ ‎padding‎ هو ‎any‎. ما يعني أنّنا نستطيع استدعاء الدّالة ‎padLeft‎ بمعاملٍ نوعُ قيمتِه مُخالفة لكلّ من النوعين ‎number‎ و‎string‎، ولن يحدث أيّ خطأ أثناء التّرجمة:

let indentedString = padLeft("Hello world", true); // سيمرّ الخطأ على المترجم لكنّه سيفشل أثناء التنفيذ

في شيفرة البرمجة كائنيّة التوجه العادية، يُمكن تجريد النوعين عبر إنشاء تسلسل أنواع (hierarchy of types). ورغم أنّ الحلّ سيكون واضحًا أكثر، لكنّه إفراط ولا حاجة لنا به. النسخة الأصلية من ‎padLeft‎ تتميّز بأنّها تستقبل الأنواع الأوليّة (primitives) مباشرةً. واستخدامها كان وجيزًا وبسيطًا. إضافة إلى أنّ هذا الحلّ لن يُساعد في حال أردنا استخدام دالّة موجودة مسبقًا في مكان آخر. عوضًا عن استخدام النّوع ‎any‎، يُمكننا استعمال نوع اتّحادٍ للمُعامل ‎padding‎:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // يُطلق خطأً أثناء الترجمة

يصِف نوع الاتحاد قيمةً يُمكن لها أن تكون من عدّة أنواع. نستعمل المحرف ‎|‎ للفصل بين كلّ نوع، لذا فالنّوع ‎number | string | boolean‎ هو نوعُ قيمة يُمكن لها أن تكون إمّا عددًا، أو سلسلة نصيّة، أو قيمة منطقيّة.

إن كانت هناك قيمة ذات نوعِ اتّحاد، فسنستطيع الوصول إلى العناصر التي تكون مشتركة بين كل الأنواع في الاتحاد فقط:

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // مسموح
pet.swim();    // خطأ

قد تكون أنواع الاتّحاد في هذه الحالة صعبة الفهم، لكنّها تحتاج فقط إلى القليل من الممارسة للتعوّد عليها. إذا كان لقيمةٍ النوعُ ‎A | B‎، فإنّنا نعرف فقط يقينًا أنّها تحتوي على العناصر الموجودة في كلّ من ‎A‎ و‎B‎ في نفس الوقت. في هذا المثال، النوع ‎Bird‎ يملك عنصرًا باسم ‎fly‎. لا يُمكننا التيقّن من أنّ المتغيّر ذو النّوع ‎Bird | Fish‎ يملك تابعًا باسم ‎fly‎. إن كان المتغيّر من النّوع ‎Fish‎ أثناء التّنفيذ، فسيفشل الاستدعاء ‎‎pet.fly()‎‎.

حرّاس الأنواع (Type Guards) وأنواع التمييز (Differentiating Types)

أنواع الاتّحاد مفيدة في الحالات التي يُمكن فيها للقيم أن تكون من أكثر من نوع واحد. لكن ماذا عن الأوقات التي نحتاج فيها إلى معرفة ما إذا كان نوعُ القيمةِ النوعَ ‎Fish‎ بالتحديد؟ التحقّق من وجود عنصر مُعيّن في الكائن طريقةٌ شائعة للتمييز بين قيمتين قد تختلفان. وكما سبق ذكره، يُمكننا فقط الوصول إلى العناصر التي نعرِف يقينًا أنّها موجودة في جميع الأنواع الموجودة في نوع الاتّحاد.

let pet = getSmallPet();

// ستفشل جميع محاولات الوصول إلى الخاصيات التاليّة
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

لإصلاح الشيفرة وجعلها تعمل، سنحتاج إلى استعمال إثبات نوع (type assertion) كما يلي:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

حراس الأنواع المعرَّفة من طرف المبرمج

لاحظ في المثال أعلاه أنّه قد توجّب علينا استخدام إثباتات الأنواع عدّة مرّات. سيكون من الجميل لو استطعنا معرفة نوع ‎pet‎ آليًا بعد التحقق من ذلك دون الحاجة إلى إعادة نفس إثبات النوع.

تملك TypeScript ميّزةً تُسمّى بحارس الأنواع (type guard). وحارس الأنواع تعبيرٌ يُؤدّي تحقّقًا أثناء التنفيذ (runtime) يضمن ثبات النّوع في مجال مُعيّن (أي أنّنا سنعرف أنّه من نوعٍ واحد في المجال وأنّ النوع لن يتغيّر ما دمنا في نفس المجال). لتعريف حارس أنواع نحتاج فقط إلى تعريف دالة يكون نوعها المُعاد (return type) عبارة نوع منطقيّة (type predicate):

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

العبارةُ ‎pet is Fish‎ هي عبارة النوع المنطقيّة في هذا المثال. تكون العبارة المنطقيّة على الشّكل ‎parameterName is Type‎، بحيث يجب على ‎parameterName‎ أن يكون اسم مُعامل من توقيع الدالة الحاليّة، و‎Type‎ هو النّوع.

في كلّ مرّة تُستدعى فيها الدّالة ‎isFish‎ بمتغيّر ما، فستحصر TypeScript هذا المتغيّر ليكون نوعُه النّوع المُحدَّد إذا كان النوع الأصليّ متوافقًا.

// سيُسمح الآن باستدعاء كلّ من
// 'swim' و 'fly'
if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

لاحظ أنّ TypeScript استنتجت أنّ نوع ‎pet‎ هو النّوع ‎Bird‎ في مجال ‎else‎، وذلك لأنّ اللغة تُدرك أنّ نوعَ ‎pet‎ هو النّوع ‎Fish‎ في مجال ‎if‎، لذا فإن لم يكن نوعُ ‎pet‎ النّوع ‎Fish‎ فلا بد له أن يكون النوع ‎Bird‎.

حراس الأنواع باستعمال ‎typeof

لنعد إلى نسخة ‎padLeft‎ التي تستعمل اتحاد الأنواع. يُمكننا إعادة كتابتها بعبارة نوع منطقيّة كما يلي:

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

لكن رغم ذلك، تعريف دالّة فقط لمعرفة ما إذا كان نوعُ قيمةٍ ما نوعًا أوليًّا يُعقّد الأمور. ولحسن الحظ لا تحتاج إلى تجريد الشّرط ‎typeof x === "number"‎ إلى دالّة خاصّة لأنّ TypeScript ستتعرّف عليه كحارس أنواع لذاته. ما يعني أنّنا نستطيع التحقّق من الأنواع في نفس السّطر كما يلي:

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

يُتعرَّف على حرّاس الأنواع ‎typeof‎ على شكلين: ‎typeof v === "typename"‎ و‎typeof v !== "typename"‎، بحيث لا بدّ للقيمة ‎"typename"‎ أن تكون إمّا ‎"number"‎، أو ‎"string"‎، أو ‎"boolean"‎، أو ‎"symbol"‎. ورغم أنّ TypeScript لن توقِفَك إن حاولت المُقارنة بسلسلة نصيّة أخرى، إلّا أنّ اللغة لن تتعرّف على التعبير كحارس أنواع.

حراس الأنواع باستعمال ‎instanceof

إن فهمتَ الفقرة السابقة وإذا سبق لك وأن تعرّفت على عامل ‎instanceof‎ في JavaScript، فأغلب الظن أنّ لك فكرةً عامّة حول هذه الفقرة.

حراسة الأنواع باستعمال ‎instanceof‎ طريقةٌ لحصر الأنواع حسب الدالة البانيّة (constructor) للنوع. مثلًا، لنعد إلى مثال مُضيف الحاشية السابق:

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// النّوع هو
// 'SpaceRepeatingPadder | StringPadder'

let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    // حُصِرَ النوع إلى
    // 'SpaceRepeatingPadder'
    padder;
}
if (padder instanceof StringPadder) {
    // حُصِرَ النوع إلى
    // 'StringPadder'
    padder;
}

يجب على الجانب الأيسر للعامل ‎instanceof‎ أن يكون دالة بانية، وستحصر TypeScript النوع إلى:

  • نوع خاصيّة ‎prototype‎ الخاصّة بالدالة إن لم يكن نوعها النوعَ ‎any‎.
  • اتحاد الأنواع التي يُعيدها توقيع المنشأ (construct signatures) الخاص بالنوع.

والترتيب مهمّ هنا.

أنواع القيم الفارغة (Nullable types)

هناك نوعان مميَّزان في TypeScript، وهما ‎null‎ و‎undefined‎، وقيمتهما هما ‎null‎ و‎undefined‎ بنفس التّرتيب (أي أنّهما نوعان وقيمتان في نفس الوقت). سبق وأن ذكرنا في صفحة الأنواع الأساسيّة أنّ مُدقِّق الأنواع (type checker) يعدّ ‎null‎ و‎undefined‎ قابلين للتعيين لأي نوع كيفما كان. ما يعني أنّ ‎null‎ ‎ و‎undefined‎ قيمتان صالحتان لجميع الأنواع. هذا يعني أنّه لا يُمكن إيقاف عمليّة التعيين لأيّ نوع، حتى ولو أردت ذلك. يُسمّي Tony Hoare مُخترعُ القيمة ‎ nullهذه المسألة بخطأ المليار دولار.

خيار المُترجِم ‎‎--strictNullChecks‎‎ حلٌّ لهذه المشكلة: عند التصريح عن متغيّر، فلن يشمل تلقائيّا النوعين ‎null‎ و‎undefined‎. لكن يُمكنك إضافتهما بصراحة عبر استخدام نوع اتّحاد كما يلي:

let s = "foo";
// خطأ، لا يمكن تعيين
// 'null'
// للنوع
// 'string'
s = null;
let sn: string | null = "bar";
sn = null; // مسموح

// خطأ، لا يمكن تعيين
// 'undefined'
// للنوع
// 'string | null'
sn = undefined;

لاحظ أنّ TypeScript تُعامِل ‎null‎ و‎undefined‎ بشكل مختلف لتُوافِق آليّة عمل لغة JavaScript. والنّوع ‎string | null‎ مُخالف للنوع ‎string | undefined‎ والنوعِ ‎string | undefined | null‎.

المعاملات والخاصيّات الاختياريّة

عند استعمال الخيار ‎‎--strictNullChecks‎‎، فالمعاملات الاختياريّة تُضيف تلقائيّا ‎‎| undefined‎‎:

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
// خطأ، النوع
// 'null'
// غير قابل للتعيين للنوع
// 'number | undefined'
f(1, null);

والشيء نفسه صحيح للخاصيات الاختياريّة:

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
// خطأ، لا يُمكن تعيين النّوع
// 'undefined'
// للنوع
// 'number'
c.a = undefined;
c.b = 13;
c.b = undefined; // مسموح
// خطأ، لا يُمكن تعيين النّوع
// 'null'
// للنوع
// 'number | undefined'
c.b = null;

حرّاس الأنواع (Type guards) وإثباتات الأنواع (type assertions)

لأنّ الأنواع التي تقبل قيمًا فارغة (nullable types) مُطبَّقةٌ باتحاد أنواع، فستحتاج إلى استعمال حارس أنواع للتخلّص من النوع ‎null‎. وهي نفس الشيفرة التي ستكتبها بلغة JavaScript:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

التخلص من ‎null‎ في المثال أعلاه واضح جدًّا، لكن يُمكنك كذلك استعمال العامل ‎||‎:

function f(sn: string | null): string {
    return sn || "default";
}

في الحالات التي لا يُمكن فيها للمترجم التخلّص من ‎null‎ أو ‎undefined‎، يُمكنك استعمال عامل إثبات النوع لحذفها يدويًّا. وذلك بإلحاق المحرف ‎!‎. مثلًا: ‎‎identifier!‎‎ يحذف ‎null‎ و‎undefined‎ من نوعِ ‎identifier‎:

function broken(name: string | null): string {
  function postfix(epithet: string) {
// خطأ، قد يكون
// 'name'
// من النّوع
// null
    return name.charAt(0) + '.  the ' + epithet;
  }
  name = name || "Bob";
  return postfix("great");
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // مسموح
  }
  name = name || "Bob";
  return postfix("great");
}

المثال يستخدم دالّة متداخلة هنا لأن المترجم غير قادر على حذف النوع ‎null‎ داخل دالة متداخلة (باستثناء الدوال التي تُستدعى مُباشرة [immediately-invoked function]). هذا لأنّ المترجم غير قادر على تتبّع جميع استدعاءات الدّالة المتداخلة، خاصّة إن أعدتها من الدالة الخارجيّة. ولأنّ المترجم لا يعلم المكان الذي ستُستدعى فيه الدّالة، فمن غير الممكن معرفة نوع ‎name‎ في الوقت الذي يُنفّذ فيه جسم الدالة.

أسماء الأنواع البديلة (Type Aliases)

تُنشئ أسماء الأنواع البديلة اسمًا جديدًا لنوع ما. أسماء الأنواع البديلة مُشابهة أحيانًا للواجهات (interfaces)، إلّا أنّها تستطيع تسمية الأنواع الأوليّة، أنواع الاتّحاد، والصفوف (tuples)، وأي نوع آخر قد تحتاج إلى كتابته يدويًّا:

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === "string") {
        return n;
    }
    else {
        return n();
    }
}

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

وكما الواجهات، يُمكن لأسماء الأنواع البديلة أن تكون عموميّة (generic) كذلك، لا نحتاج سوى لإضافة معاملات أنواعٍ واستعمالها على الجانب الأيمن من التصريح عن الاسم البديل:

type Container<T> = { value: T };

يُمكن كذلك لاسم بديل أن يُشير إلى نفسه في خاصيّة:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

وبجمعه مع أنواع التقاطع، يُمكننا الحصول على أنواع معقّدة أكثر:

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

لكن لا يُمكن لاسم بديل أن يظهر في أي مكان آخر على الجانب الأيمن من التصريح:

type Yikes = Array<Yikes>; // خطأ

الواجهاتُ مُقابلَ أسماءِ الأنواع البديلة

كما سبق ذكره، يُمكن لأسماء الأنواع البديلة العمل بشكل مشابه للواجهات؛ لكن هناك بعض الاختلافات الصغيرة.

أحد الاختلافات أنّ الواجهات تنشئ اسمًا جديدًا يُستخدَم في كل مكان. أسماء الأنواع البديلة لا تنشئ اسمًا جديدًا، على سبيل المثال، رسائل الخطأ لن تستعمل الاسم البديل. إن مرّرت مؤشّر الفأرة على الدالة ‎interfaced‎ في الشيفرة أدناه في مُحرّر الشيفرة فسيعرض المُحرّر أنّها ستُعيد كائنًا من النّوع ‎Interface‎، أمّا في حالة ‎aliased‎ فالكائن المُعاد سيكون نوع قيمة كائن حرفيّة (object literal type).

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

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

أمّا إن لم تستطع التعبير عن شكل بواجهة وتحتاج إلى نوع اتحاد أو نوع صفّ (tuple type)، فأسماء الأنواع البديلة حلّ جيّد.

أنواع السلاسل النصيّة الحرفيّة (String Literal Types)

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

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // خطأ، لا يجب تمرير
            // null أو undefined
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
// خطأ، لا يُسمح للنوع
// "uneasy"
button.animate(0, 0, "uneasy");

يُمكنك تمرير أي من السلاسل النصيّة الثلاثة المسموح بها، لكن أي سلسلة نصيّة أخرى ستطلق خطأ:

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

يُمكن استخدام أنواع السلاسل النصيّة الحرفيّة بنفس الطريقة للتمييز بين الأحمال الزّائدة:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... المزيد من الأحمال الزائدة ...
function createElement(tagName: string): Element {
    // ... جسم الدّالة ...
}

الأنواع العدديّة الحرفيّة (Numeric Literal Types)

يُمكن كذلك استعمال الأنواع العدديّة الحرفيّة في TypeScript:

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}

نادرًا ما تُكتب هذه الأنواع العدديّة مُباشرة، لكن يُمكن لها التعرّف على العِلَلْ:

function foo(x: number) {
    if (x !== 1 || x !== 2) {
        //         ~~~~~~~
        // لا يُمكن تطبيق العامل
        // '!=='
        // على النوعين
        // '1'
        // و
        // '2'
    }
}

بعبارة أخرى، لا بد للمتغيّر ‎x‎ أن يحمل القيمة ‎1‎ عندما يُقارن بالقيمة ‎2‎، لذا فالتحقّق يقوم بمقارنة غير صالحة.

أنواع عناصر الثوابت المتعدّدة (Enum Member Types)

ذُكِر في صفحة الثوابت المتعدّدة أنّ عناصر الثوابت المتعدّدة تملك أنواعًا عندما يكون كل عنصر مُهيأً بقيمة حرفيّة.

في معظم الحالات التي نتحدث فيها عن الأنواع الفرديّة (singleton types)، فإنّنا نُشير إلى كلّ من أنواع عناصر الثوابت المتعدّدة إضافة إلى الأنواع العدديّة الحرفيّة وأنواع السلاسل النصيّة الحرفيّة، لكن معظم المستخدمين يُشيرون إلى الأنواع الفرديّة والأنواع الحرفيّة (literal types) على أنّ لها نفس المعنى.

الاتحادات المتقطعة (Discriminated Unions)

يُمكن مزج الأنواع الفرديّة، وأنواع الاتحاد، وحراس الأنواع، و أسماء الأنواع البديلة لبناء نمط مُتقدّم يُسمّى الاتحادات المتقطعة، ويُعرف كذلك بالاتحادات المرقومة (tagged unions)، أو أنواع البيانات الجبريّة (algebraic data types). الاتحادات المتقطعة مفيدة في البرمجة الوظيفيّة (functional programming). وبعض اللغات تُحول الاتحادات إلى اتحادات متقطعة تلقائيًّا؛ أمّا TypeScript فتبني فوق أنماط JavaScript الموجودة حاليًّا. وهناك ثلاثة مكوّنات:

  1. الأنواع التي تملك خاصيّة نوع فرديّة مشتركة: المُميِّز (discriminant).
  2. اسم نوع بديل يأخذ اتحاد هذه الأنواع: الاتحاد (union).
  3. حراس أنواع على الخاصية المشتركة.
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

نُصرّح أولًا عن الواجهات التي ستُضاف إلى الاتحاد. تحمل كل واجهة خاصيّة باسم ‎kind‎، كل خاصيّة تحمل نوع سلسلة نصيّة حرفيّة مختلفًا. الخاصيّة ‎kind‎ هنا تُسمّى المميِّز (discriminant) أو الوسم (tag). والخاصيات الأخرى فخاصّة بكلّ واجهة. لاحظ أنّ الواجهات غير مترابطة حاليًّا. لنضعها في اتحاد:

type Shape = Square | Rectangle | Circle;

لنستخدم الآن اتحادًا متقطعًا:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

التحقّق من الشموليّة

يُمكن أن نُخطئ وننسى التحقّق من أحد أشكال الاتحاد المتقطع، ما يعني أنّ التحقّق لن يكون شموليًّا. نُريد من المترجم أن يُخبرها عندما لا نتحقّق من جميع أشكال الاتحاد المتقطع. مثلًا، إن أضفنا النوع ‎Triangle‎ إلى ‎Shape‎، فسنحتاج إلى تحديث الدالة ‎area‎ كذلك:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
    // يجب أن يُطلَق خطأ هنا لأنّنا لم نُعالج حالة
    // "triangle"
}

هناك طريقتان للقيام بالأمر. الأولى أن نُفعّل الخيار ‎--strictNullChecks‎ ونُحدّد نوعًا مُعادًا:

// خطأ، الدالة تُعيد
// number | undefined
function area(s: Shape): number {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

ولأنّ جملة ‎switch‎ لم تعد شموليّة، فإنّ TypeScript تدري أنّ الدالة قد تُعيد القيمة ‎undefined‎. إن حُدّد النوع المعاد ليكون النوع ‎number‎ صراحةً، فستحصل على خطأ يُنبّه إلى أنّ النوع المعاد هو النوعُ ‎number | undefined‎ في حقيقة الأمر وليس النوعَ ‎number‎. لكن هذه الطريقة غامضة نوعًا ما، إضافةً إلى أنّ الخيار ‎--strictNullChecks‎ لا يعمل دائمًا مع الشيفرة القديمة.

الطريقة الثانيّة تستعمل النوع ‎never‎ الذي يستخدمه المترجم للتحقّق من الشموليّة:

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // يُطلَق خطأ هنا إن كان هناك حالات مفقودة
    }
}

هنا تتحقّق الدالة ‎assertNever‎ من أنّ ‎s‎ من النّوع ‎never‎ (وهو النوع الذي يبقى بعد حذف جميع الحالات الأخرى)؛ إن نسيت حالة ما، فسيكون ‎s‎ من نوعٍ حقيقيّ وستحصل على خطأ. تتطلّب هذه الطريقة تعريف دالة إضافيّة، لكنّ هذه الطريقة أوضح من سابقتها.

أنواع ‎this‎ متعدّدة الأشكال

يُمثّل نوعُ ‎this‎ متعدّد الأشكال نوعًا يكون نوعًا فرعيًّا (subtype) من الصنف أو الواجهة الحاوية. يُسمّى هذا بالتعبير F-bounded polymorphism. ويسمح هذا بالتعبير عن الواجهات التسلسلية الفصيحة (hierarchical fluent interfaces) بطريقة أسهل، على سبيل المثال. لنأخذ حاسبةً بسيطة تُعيد ‎this‎ بعد كل عمليّة حسابيّة:

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... يُمكن إضافة العمليات الأخرى هنا ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

لأنّ الصنف يستخدم أنواع ‎this‎، فيُمكنك توسيعه، وسيتمكّن الصنف الجديد من استخدام التوابع القديمة دون تغيير.

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... يُمكن إضافة العمليات الأخرى هنا ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

لو لم تُستَخدم أنواع ‎this‎ هنا، لَمَا تمكّن الصّنف ‎ScientificCalculator‎ من توسيع الصّنف ‎BasicCalculator‎ وإبقاء الواجهة الفصيحة. ولَأعاد التابعُ ‎multiply‎ الصنفَ ‎BasicCalculator‎ الذي لا يمتلك التابعَ ‎sin‎. لكن مع استخدام أنواع ‎this‎، فالتابع ‎multiply‎ يُعيد ‎this‎، والذي يكون من النوع ‎ScientificCalculator‎ في هذه الحالة.

أنواع الفهرس (Index types)

يُمكن باستخدام أنواع الفهرس جعلُ المترجم على التحقّق من الشيفرة التي تستخدم أسماء الخاصيات الديناميكيّة. مثلًا، انتقاء مجموعة من الخاصيات من كائن معيّن من أنماط Javascript الشائعة:

function pluck(o, names) {
    return names.map(n => o[n]);
}

إليك كيفيّة كتابة واستخدام هذه الدالة في TypeScript، وذلك باستخدام عاملَيْ استعلام نوع الفهرس (index type query) والوصول عبر الفهرس (indexed access).

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name']); // مسموح به, string[]

يتحقّق المترجم من أنّ ‎name‎ خاصية موجودة بالفعل على الكائن ‎Person‎. يُقدّم المثال بعض عاملات الأنواع الجديدة. أولًا ‎keyof T‎ والذي هو عامل استعلام نوع الفهرس. لكلّ نوعٍ ‎T‎، فإنّ ‎keyof T‎ هو اتّحادُ أسماء الخاصيات العامّة المعروفة في النوع ‎T‎. مثلًا:

let personProps: keyof Person; // 'name' | 'age'

يُمكن مُبادلة ‎keyof Person‎ مع ‎'name' | 'age'‎ لأنّ لهما نفس القيمة. الفرقُ يظهر في حالة أضفنا خاصية أخرى إلى ‎Person‎، فلو أضفنا مثلًا ‎address: string‎، عندها ستُحدَّث قيمة ‎keyof Person‎ تلقائيّا لتُصبح ‎'name' | 'age' | 'address'‎. ويُمكنك استخدام ‎keyof‎ في سياقات عموميّة كما فعلنا مع الدالة ‎pluck‎، أي في الحالات التي لا يُمكنك فيها معرفة أسماء الخاصيات مسبقًا. ما يعني أنّ المترجم سيتحقّق من أنّ مجموعة أسماء الخاصيّات الممرّرة إلى الدالة ‎pluck‎ مجموعةٌ صحيحة:

// خطأ، القيمةُ
// 'unknown'
// لا توجد في الاتحاد
// 'name' | 'age'
pluck(person, ['age', 'unknown']);

العامل الثّاني هو العامل ‎T[K]‎ وهو عامل الوصول عبر الفهرس. هنا، تعكس بنية النوع (type syntax) بنية التعبير (expression syntax). هذا يعني أنّ ‎person['name']‎ من النّوع ‎Person['name']‎ (أي ‎string‎ في هذا المثال). لكن يُمكنك (كما في استعلام نوع الفهرس) استخدام ‎T[K]‎ في سياق عمومي والذي يُظهر قوّة وفعاليّة هذا العامل حقيقةً. عليك فقط التأكّد من أنّ متغيّر النّوع ‎K‎ يُوسّع الاتحاد ‎keyof T‎ بالجملة ‎K extends keyof T‎. إليك مثالًا آخر لدالة باسم ‎getProperty‎:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

في الدالة ‎getProperty‎، لدينا ‎o: T‎ (الكائن) و‎name: K‎ (اسم الخاصية المرغوبة)، ما يعني ‎o[name]: T[K]‎. وحالما تُعيد النتيجةَ ‎T[K]‎، فسيُهيّء المترجم النوعَ الحقيقيّ للمفتاح، لذا فالنوع المعاد للدالة ‎getProperty‎ سيختلف حسب الخاصيّة التي تطلبها.

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
// خطأ، القيمةُ
// 'unknown'
// لا توجد في الاتحاد
// 'name' | 'age'
let unknown = getProperty(person, 'unknown');

أنواع الفهرس (Index types) وتوقيعات الفهارس النصيّة (string index signatures)

يتفاعل العامل ‎keyof‎ والعامل ‎T[K]‎ مع توقيعات الفهارس النصيّة. إن كان لديك نوع ذو توقيع فهرس نصيّ، فنوعُ ‎keyof T‎ هو النّوع ‎string‎ ببساطة. و‎T[string]‎ هو نوع توقيع الفهرس:

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

الأنواع المخطَّطَة (Mapped types)

تحويلُ كل خاصية من خاصيات نوعٍ إلى خاصيّة اختيارية من الأعمال الشائعة في البرمجة:

interface PersonPartial {
    name?: string;
    age?: number;
}

أو قد نحتاج إلى نسخة قابلة للقراءة فقط:

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

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

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

ولاستخدامها:

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

لنلق نظرة على أبسط نوع مخطَّط وأجزاءه:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

البنية هنا مشابهة لبنية توقيعات الفهرس مع جملة ‎‎for .. in‎‎ داخل التوقيع. هذه هي الأجزاء الثلاثة:

  1. متغيّر النوع ‎K‎، والذي يُربَط بكل خاصيّة من الخاصيّات في كل مرّة.
  2. ‎،Keys‎ وهي مفاتيح اتحاد السلاسل النصية الحرفيّة، والتي تحتوي على أسماء الخاصيات للتكرار (iterate) عليها.
  3. النوع الناتج للخاصيّة.

في هذا المثال البسيط، نكتب المفاتيح Keys بصراحة على شكل قائمة من أسماء الخاصيات ونوع الخاصيّة هو دائمًا النوع ‎boolean‎، لذا فهذا النوع المخطَّط مُكافئ لما يلي:

type Flags = {
    option1: boolean;
    option2: boolean;
}

لكن التطبيقات على الواقع تبدو مثل ‎Readonly‎ و‎Partial‎ أعلاه. أي أنّها تُبنى على نوع موجود مسبقًا، وتُحوِّل الخاصيات على نحوٍ معيّن. هنا يُمكننا استغلال ‎keyof‎ والوصول إلى الأنواع عبر الفهارس:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

ونسخة عامّة أنفع لقابليّة إعادة استخدامها مع أكثر من نوع واحد:

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

في هاذين المثالين، قائمة الخاصيات هي ‎keyof T‎ والنوع الناتج هو نسخة ذات شكل مختلف من النوع ‎T[P]‎. هذا قالب (template) جيّد لأي استخدام عام للأنواع المخطَّطة. وذلك لكون هذا النّوع متماثل الشّكل (homomorphic)، ما يعني أنّ التخطيط يُطبّق على خاصيّات ‎T‎ فقط لا غير. يعرف المترجم أنّه قادر على نسخ جميع مُحدِّدات الخاصيّات (property modifiers) قبل إضافة أخرى جديدة. مثلًا، إذا كانت الخاصيّة ‎Person.name‎ قابلةً للقراءة فقط، وحوّلنا خاصيّات ‎Person‎ إلى اختياريّة باستعمال ‎Partial‎ فالخاصيّة ‎Partial<Person>.name‎ ستكون قابلة للقراءة فقط واختياريّة في الآن ذاته.

إليك مثالًا أخيرًا، هنا نُحيط ‎T[P]‎ بصنف وسيط ‎Proxy<T>‎:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... جسم الدالة ...
}
let proxyProps = proxify(props);

لاحظ أنّ ‎Readonly<T>‎ و‎Partial<T>‎ مفيدة جدًّا، لذا فهي موجودة في مكتبة TypeScript القياسيّة، إضافة إلى ‎Pick‎ و‎Record‎:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}

كل من ‎Readonly‎، و‎Partial‎، و‎Pick‎ متماثلة الشكل، لكنّ ‎Record‎ ليس كذلك. وممّا يدلّ على أنّ ‎Record‎ ليس متماثل الشكل أنّه لا يأخذ نوعًا مُدخلًا لنسخ الخاصيات منه:

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

الأنواع التي لا تكون متماثلة الشكل تُنشئ خاصيات جديدة، لذا لا يمكنها نسخ محدّدات الخاصيات (property modifiers) من أي مكان آخر.

الاستنتاج من الأنواع المخطَّطة

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

function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);

لاحظ أنّ استنتاج الأنواع عبر فكّ الإحاطة عنها يعمل فقط على الأنواع المخطَّطة المتماثلة الشّكل. إن كان النوع المخطَّط غير متماثل الشكل، فستحتاج إلى إضافة معامل نوع صريح إلى دالة فكّ الإحاطة الخاصّة بك.

الأنواع الشرطيّة (Conditional Types)

أضافت النسخة 2.8 من لغة TypeScript ميّزة الأنواع الشرطيّة التي تسمح لنا بالتعبير عن اقترانات أنواع غير موحّدة. إذ يختار النوع الشرطيّ نوعًا واحدًا من نوعين مُحتمَلَين حسب شرطٍ يُعبَّر عنه باختبارِ علاقةٍ بين نوعين كما يلي:

T extends U ? X : Y

النوع أعلاه يعني: إذا كان النوعُ ‎T‎ قابلًا لتعيينه للنوع ‎U‎ فالنوعُ النهائي المُقرَّر سيكون النوعَ ‎X‎، أمّا إن لم يتحقّق الشّرط، فالنوعُ هو ‎Y‎.

النوع الشّرطي ‎T extends U ? X : Y‎ يُقرَّر إلى ‎X‎ أو ‎Y‎، أو يُؤجَّل القرار إذا اعتمد الشّرط على متغيّر نوع واحد أو أكثر. إذا احتوى كل من ‎T‎ أو ‎U‎ على متغيّرات أنواع، فتقرير النوعِ النهائي ليكون ‎X‎ أو ‎Y‎ أو التأجيل يُحدَّدُ عبر ما إذا امتلك نظام الأنواع معلومات كافيّة لاستنتاج قابليّة تعيين النوع ‎T‎ للنوع ‎U‎ في جميع الحالات (أي لا بد للشرط أن يتحقّق دائمًا).

مثال على أنواع تُقرَّر مباشرة (أي دون تأجيل):

declare function f<T extends boolean>(x: T): T extends true ? string : number;

// النوع المُقرَّر هو
// string | number

let x = f(Math.random() < 0.5)

اسم النوع البديل ‎TypeName‎ مثال آخر، هذه المرّة نستخدم أنواعًا شرطيّة متداخلة:

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"

مثال على حالة يُؤجَّل فيها تقرير الأنواع الشّرطيّة (والتي تبقى فيها الأنواع الشرطيّة كما هي دون تقرير نوعٍ مُحدّد):

interface Foo {
    propA: boolean;
    propB: boolean;
}

declare function f<T>(x: T): T extends Foo ? string : number;

function foo<U>(x: U) {
    // النوعُ النهائي هو النوع الشّرطي
    // 'U extends Foo ? string : number'
    let a = f(x);

    // لكن هذا التعيين مسموح به
    let b: string | number = a;
}

في المثال أعلاه نوعُ المتغيّرِ ‎a‎ لم يُقرَّر بعد. إذا استُدعيَت الدالة ‎foo‎، فسيُستبدل النّوع ‎U‎ بنوع آخر، وستُعيد TypeScript تقدير النوع الشرطي وتقرير نوعه النهائي.

لكن لا يزال بإمكاننا تعيين نوع شرطيّ إلى أي نوعٍ هدفٍ ولو أُجِّل تقرير النّوع ما دام كل فرع من فرعي الشرط قابلًا للتعيين لهذا النوع الهدف. لذا استطعنا تعيين النوع ‎U extends Foo ? string : number‎ للنوع ‎string | number‎ في مثالنا أعلاه، وذلك لأنّنا ندري أنّ الشرط سينتهي في الأخير إمّا إلى النوع ‎string‎ أو إلى النوع ‎number‎.

الأنواع الشرطية التوزيعية (Distributive conditional types)

تُسمّى الأنواع الشرطيّة التي يكون فيها النوع المُتحقَّق منه معاملَ نوعٍ مجرّدٍ (naked type parameter) بالأنواع الشّرطية التوزيعيّة. وهي أنواع تُوزَّع تلقائيًّا على أنواع اتّحاد عند التهيئة. مثلًا، تهيئة النوع ‎T extends U ? X : Y‎ مع تمرير النّوع ‎A | B | C‎ كقيمة للمعامل ‎T‎ توزّع لتصبح على الشّكل ‎(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)‎.

مثال:

type T10 = TypeName<string | (() => void)>;  // "string" | "function"
type T12 = TypeName<string | string[] | undefined>;  // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>;  // "object"

عند تهيئة النوع الشرطي التوزيعي ‎T extends U ? X : Y‎، فالمراجع التي تشير إلى النوع ‎T‎ داخل النوع الشرطي تقرَّر إلى مكوّنات نوع الاتحاد الفردية (أي أنّ النوع ‎T‎ يشير إلى المكونات الفردية بعد توزيع النوع الشرطي على نوع الاتحاد). إضافةً إلى أنّ المراجع التي تُشير إلى النوع ‎T‎ داخل النوع ‎X‎ تملك قيد معامل أنواع إضافيّ، وهو معامل النوع ‎U‎ (يُعدّ النوع ‎T‎ قابلا للتعيين للنوع ‎U‎ داخل ‎X‎).

مثال:

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;

type T20 = Boxed<string>;  // BoxedValue<string>;
type T21 = Boxed<number[]>;  // BoxedArray<number>;
type T22 = Boxed<string | number[]>;  // BoxedValue<string> | BoxedArray<number>;

لاحظ أنّ النوع ‎T‎ يمتلك القيد الإضافيّ ‎any[]‎ على الفرع الصحيح من النوع ‎Boxed<T>‎ لذا فمن الممكن الإشارة إلى نوع عناصر المصفوفة بالتعبير ‎T[number]‎. لاحظ كذلك كيف أن النوع الشرطي قد وُزِّع على نوع الاتحاد في السطر الأخير.

يُمكن استعمال ميّزة التوزيع في الأنواع الشرطية لترشيح أنواع الاتحاد:

// Remove types from T that are assignable to U
// حذف الأنواع من النوع
// T
// القابلة للتعيين للنوع
// U
type Diff<T, U> = T extends U ? never : T;
// Remove types from T that are not assignable to U
// حذف الأنواع من النوع
// T
// غير القابلة للتعيين للنوع
// U
type Filter<T, U> = T extends U ? T : never;

type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
type T32 = Diff<string | number | (() => void), Function>;  // string | number
type T33 = Filter<string | number | (() => void), Function>;  // () => void

// Remove null and undefined from T
// حذف النوعين
// null و undefined
// من النوع
// T

type NonNullable<T> = Diff<T, null | undefined>;

type T34 = NonNullable<string | number | undefined>;  // string | number
type T35 = NonNullable<string | string[] | null | undefined>;  // string | string[]

function f1<T>(x: T, y: NonNullable<T>) {
    x = y;  // مسموح به
    y = x;  // خطأ
}

function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
    x = y;  // مسموح به
    y = x;  // خطأ
    let s1: string = x;  // خطأ
    let s2: string = y;  // مسموح به
}

الأنواع الشرطية مفيدة جدا عند مزجها مع الأنواع المخطَّطة:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string): void;
}

type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T41 = NonFunctionPropertyNames<Part>;  // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>;  // { id: number, name: string, subparts: Part[] }

لا يجوز الإشارة إلى الأنواع الشرطيّة تعاوديًّا (recursively) كما هي الحال مع أنواع الاتحاد وأنواع التقاطع. المثال التالي خطأ:

type ElementType<T> = T extends any[] ? ElementType<T[number]> : T;  // خطأ

استنتاج الأنواع في الأنواع الشّرطيّة

يُمكن وضع تصريحات ‎infer‎ داخل جملة ‎extends‎ الخاصة بالنوع الشرطي، تُستخدَم تصريحات ‎infer‎ لاستنتاج متغيّر نوع معيّن. يُمكن الإشارة إلى متغيّرات الأنواع المُستَنتجَة في الفرع الصحيح من النوع الشرطي. ويُمكن كذلك استعمال تصريحات ‎infer‎ في عدّة أماكن لنفس متغيّر النوع.

مثلًا، يستخرج النوع التالي النوع المعاد (return type) من نوع دالّة (function type):

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

هنا يُستنتَج النوع ‎R‎ ويكون تلقائيّا النوع المعاد الموجود في نوع الدالة المعطى، فمثلًا لو أعادت الدالة سلسلة نصيّة، فسيُشير متغيّر النوع ‎R‎ إلى النّوع ‎string‎.

يُمكن للأنواع الشرطية أن تتداخل لتشكيل سلسلة من موافقات الأنماط (pattern matches) تُقدَّر بالترتيب:

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

يوضّح المثال التالي كيف يُستنتج نوع اتحاد في حالة كانت هناك عدّة أنواع مرشّحَة (candidates) لنفس متغيّر النوع في أماكن متغايِرة (co-variant):

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

وبالمِثل، عدّة أنواع مرشّحة لنفس متغير النوع في الأماكن غير المتغايرة (contra-variant) تؤدي إلى استنتاج نوع تقاطع:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

عند الاستنتاج من نوع ذي تواقيع استدعاء (call signatures) متعدّدة (مثل نوع دالة ذات أحمال زائد)، فإنّ الاستنتاج يُؤخَذ من آخر توقيع (والذي يفترض أن يكون أكثر توقيع متساهل). لا يمكن تقرير الأحمال الزائدة (overload resolution) بناءً على قائمة أنواع معاملات.

declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
type T30 = ReturnType<typeof foo>;  // string | number

لا يمكن استعمال تصريحات ‎infer‎ في جمل التقييد (constraint clauses) لمعاملات الأنواع العادية:

type ReturnType<T extends (...args: any[]) => infer R> = R;  // خطأ، العملية غير ممكنة

لكن يُمكن الحصول على نفس التأثير عبر حذف متغيرات الأنواع في التقييد واستخدام نوع شرطي عوضًا عنها:

type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;

الأنواع الشرطية مسبقة التعريف

أضافت النسخة 2.8 من لغة TypeScript عدّة أنواع شرطيّة لمكتبتها القياسية ‎lib.d.ts‎:

  • ‎Exclude<T, U>‎‎: استبعد من النوع ‎T‎ الأنواع القابلة للتعيين للنوع ‎U‎.
  • ‎Extract<T, U>‎‎: استخرج من ‎T‎ الأنواع القابلة للتعيين للنوع ‎U‎.
  • NonNullable<T>‎‎: استبعد ‎null‎ و‎undefined‎ من النوع ‎T‎.
  • ‎ReturnType<T>‎‎: احصل على النوع المعاد من نوع دالة ما.
  • ‎InstanceType<T>‎‎: احصل على نوع النسخة (instance type) من نوع دالة بانيّة (constructor function type).

أمثلة:

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // خطأ
type T18 = ReturnType<Function>;  // خطأ

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // خطأ
type T24 = InstanceType<Function>;  // خطأ

لاحظ أنّ ‎Exclude‎ هو نفسه النوع ‎Diff‎ الذي سبق وأن تعرفنا عليه، سُميَ بالاسم ‎Exclude‎ لتجنّب المشاكل التي قد تحدث في الشيفرة التي يكون فيها النوع ‎Diff‎ معرّفًا مسبقًا.

مصادر