الأنواع المتقدمة في TypeScript
أنواع التقاطع (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 الموجودة حاليًّا. وهناك ثلاثة مكوّنات:
- الأنواع التي تملك خاصيّة نوع فرديّة مشتركة: المُميِّز (discriminant).
- اسم نوع بديل يأخذ اتحاد هذه الأنواع: الاتحاد (union).
- حراس أنواع على الخاصية المشتركة.
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
داخل التوقيع. هذه هي الأجزاء الثلاثة:
- متغيّر النوع
K
، والذي يُربَط بكل خاصيّة من الخاصيّات في كل مرّة. - ،
Keys
وهي مفاتيح اتحاد السلاسل النصية الحرفيّة، والتي تحتوي على أسماء الخاصيات للتكرار (iterate) عليها. - النوع الناتج للخاصيّة.
في هذا المثال البسيط، نكتب المفاتيح 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
معرّفًا مسبقًا.