مرحبا سكان! لقد أرسلنا منتجًا جديدًا آخر هو
" Professional TypeScript. تطوير تطبيقات JavaScript قابلة للتطوير " إلى دار الطباعة . في هذا الكتاب ، سيتعلم المبرمجون المتوسطون بالفعل في JavaScript كيفية إتقان TypeScript. سترى كيف يمكن لـ TypeScript مساعدتك في قياس شفرتك بشكل أفضل بمقدار 10x وجعل البرمجة ممتعة مرة أخرى.
يوجد أدناه مقتطف من فصل من كتاب "الأنواع المتقدمة".
أنواع متقدمة
يفاجئ نظام الكتابة TypeScript المشهور عالميًا حتى مبرمجي Haskell بقدراته. كما تعلم بالفعل ، فهو ليس معبرًا فحسب ، بل سهل الاستخدام أيضًا: قيود النوع والعلاقات فيه موجزة ومفهومة ، ويتم استنتاجها تلقائيًا في معظم الحالات.
تتطلب نمذجة عناصر JavaScript الديناميكية مثل النماذج الأولية ، وربطها ، والأحمال الزائدة على الوظائف ، والكائنات المتغيرة باستمرار نظام كتابة ونظام كتابة غنيًا مثل Batman.
سأبدأ هذا الفصل بغوص عميق في موضوعات التصنيف الفرعي والتوافق والتباين والمتغير العشوائي والتمديد. ثم سأتوسع في تفاصيل فحص النوع المستند إلى التدفق ، بما في ذلك الصقل والكلية. بعد ذلك ، سأوضح بعض ميزات البرمجة المتقدمة على مستوى النوع: توصيل أنواع الكائنات وتعيينها ، واستخدام الأنواع الشرطية ، وتحديد حماية النوع ، والحلول الاحتياطية مثل تأكيدات النوع وتأكيدات التعيين الصريحة. أخيرًا ، سأقدم لك بعض الأنماط المتقدمة لتحسين أمان النوع: نقش الكائن المصاحب ، وتحسينات الواجهة لـ tuples ، وتقليد الأنواع الاسمية ، وامتداد النموذج الأولي الآمن.
العلاقات بين الأنواع
دعونا نلقي نظرة فاحصة على العلاقات في TypeScript.
الأنواع الفرعية والأنماط الفائقة
لقد تطرقنا بالفعل إلى التوافق في قسم حول الأنواع في الصفحة. 34 ، لذلك دعونا نتعمق في هذا الموضوع ، بدءًا من تعريف النوع الفرعي.

العودة إلى الشكل. 3.1 وراجع اقترانات الأنواع الفرعية المضمنة في TypeScript.

- المصفوفة هي نوع فرعي من الكائن.
- الصفيف هو نوع فرعي من المصفوفة.
- كل شيء هو نوع فرعي من أي.
- أبدا هو نوع فرعي من كل شيء.
- فئة الطيور ، والتي تمتد إلى فئة الحيوانات ، هي نوع فرعي من فئة الحيوانات.
وفقًا للتعريف الذي قدمته للتو للنوع الفرعي ، فهذا يعني أن:
- أينما كنت بحاجة إلى كائن ، يمكنك استخدام مصفوفة.
- حيثما دعت الحاجة إلى مصفوفة ، يمكن استخدام tuple.
- أينما تحتاج إلى أي شيء ، يمكنك استخدام كائن.
- لا يمكنك استخدام أبدا في كل مكان.
- حيثما تريد حيوانًا ، يمكنك استخدام Bird.
النوع الفائق هو عكس النوع الفرعي.
SUPERTYPE
إذا كان لديك نوعان ، A و B ، و B هو نوع مرتفع من A ، فيمكنك استخدام A بأمان حيثما كان B مطلوبًا (الشكل 6.2).

ومرة أخرى ، بناءً على الرسم البياني في الشكل. 3.1:
- المصفوفة هي نوع فائق من المجموعة.
- الكائن هو نوع فائق من المصفوفة.
- أي نوع فائق من كل شيء.
- أبدا ليس أي شخص فائق.
- الحيوان هو نوع من الطيور.
إنه عكس الأنواع الفرعية ولا شيء أكثر.
التباين
بالنسبة لمعظم الأنواع ، من السهل فهم ما إذا كان نوع معين A هو نوع فرعي من B. بالنسبة للأنواع البسيطة مثل الرقم والسلسلة وما إلى ذلك ، يمكنك الرجوع إلى الرسم التخطيطي في الشكل. 3.1 أو بشكل مستقل تحديد أن الرقم الموجود في رقم الاتحاد | السلسلة هي نوع فرعي من هذا الاتحاد.
لكن هناك أنواعًا أكثر تعقيدًا ، مثل الأدوية الجنيسة. ضع في اعتبارك هذه الأسئلة:
- متى يكون Array <A> نوعًا فرعيًا من Array <B>؟
- متى يكون النموذج أ نوعًا فرعيًا من النموذج ب؟
- متى تكون الوظيفة (أ: أ) => ب نوعًا فرعيًا من الوظيفة (ج: ج) => د؟
قواعد التصنيف الفرعي للأنواع التي تحتوي على أنواع أخرى (أي ، وجود معلمات نوع مثل Array <A> ، أو النماذج ذات الحقول مثل {a: number} ، أو وظائف مثل (a: A) => B) يصعب فهمها ، لأنها ليست كذلك متسقة عبر لغات البرمجة المختلفة.
لتسهيل قراءة القواعد التالية ، سأقدم بعض عناصر بناء الجملة التي لا تعمل في TypeScript (لا تقلق ، إنها ليست رياضية):
- A <: B تعني "A هو نوع فرعي من نفس النوع B" ؛
- A>: B تعني "A هو نوع فائق من نفس النوع B".
تنوع النموذج والصفيف
لفهم سبب عدم اتفاق اللغات في قواعد التصنيف الفرعي للأنواع المعقدة ، سيساعدك مثال مع نموذج يصف مستخدمًا في تطبيق ما. نحن نمثلها من خلال نوعين:
// , .
type ExistingUser = {
id: number
name: string
}
// , .
type NewUser = {
name: string
}
افترض أن أحد المتدربين في شركتك مكلف بكتابة رمز لحذف مستخدم. يبدأ بما يلي:
function deleteUser(user: {id?: number, name: string}) {
delete user.id
}
let existingUser: ExistingUser = {
id: 123456,
name: 'Ima User'
}
deleteUser(existingUser)
يتلقى deleteUser كائنًا من النوع {id؟: number، name: string} ويمرر مستخدمًا موجودًا من النوع {id: number، name: string} إليه. لاحظ أن نوع معرف الخاصية (الرقم) هو نوع فرعي من النوع المتوقع (رقم | غير محدد). لذلك ، فإن الكائن {id: number، name: string} بأكمله هو نوع فرعي من {id؟: number، name: string} ، لذلك تسمح TypeScript بذلك.
هل ترى أي مشاكل أمنية؟ هناك واحد: بعد تمرير ExistingUser إلى deleteUser ، لا يعرف TypeScript أن معرف المستخدم قد تم حذفه ، لذلك إذا قرأت CurrentUser.id بعد حذفه deleteUser (existingUser) ، فإن TypeScript لا يزال يفترض أن CurrentUser.id هو رقم من النوع.
من الواضح أن استخدام نوع كائن حيث من المتوقع أن يكون نوعه الفائق غير آمن. فلماذا تسمح TypeScript بذلك؟ خلاصة القول هي أنه لم يكن من المفترض أن تكون آمنة تمامًا. يسعى نظام الكتابة الخاص به إلى اكتشاف الأخطاء الحقيقية وجعلها مرئية للمبرمجين من جميع المستويات. نظرًا لأن التحديثات المدمرة (مثل حذف خاصية) نادرة نسبيًا في الممارسة العملية ، فإن TypeScript يكون مسترخياً ويسمح لك بتعيين كائن حيث من المتوقع أن يكون نوعه فائقًا.
وماذا عن الحالة المعاكسة: هل من الممكن تعيين كائن حيث يكون نوعه الفرعي متوقعًا؟
دعنا نضيف نوعًا جديدًا للمستخدم القديم ، ثم نزيل المستخدم بهذا النوع (تخيل إضافة أنواع إلى الكود الذي كتبه زميلك):
type LegacyUser = {
id?: number | string
name: string
}
let legacyUser: LegacyUser = {
id: '793331',
name: 'Xin Yang'
}
deleteUser(legacyUser) // TS2345: a 'LegacyUser'
//
// '{id?: number |undefined, name: string}'.
// 'string' 'number |
// undefined'.
عند إرسال نموذج بخاصية يكون نوعها نوعًا فائقًا من النوع المتوقع ، يقسم TypeScript. هذا لأن المعرف عبارة عن سلسلة | رقم | يتعامل undefined و deleteUser فقط مع الحالة التي يكون فيها المعرف رقمًا | غير معرف.
أثناء توقع نموذج ، يمكنك تمرير نوع به أنواع خصائص تكون <: من الأنواع المتوقعة ، ولكن لا يمكنك تمرير نموذج بدون أنواع الخصائص التي تعد أنواعًا فائقة لأنواعها المتوقعة. عندما نتحدث عن الأنواع ، نقول ، "أشكال TypeScript (كائنات وفئات) متغايرة في أنواع خصائصها." بمعنى أنه لكي يتم تخصيص الكائن أ للكائن ب ، يجب أن تكون كل خاصية من خصائصه <: الخاصية المقابلة في ب.
التباين المشترك هو أحد أربعة أنواع من التباين:
الثبات على
وجه التحديد مطلوب ت.
التباين المشترك
مطلوب <: T.
التباين
مطلوب>: T.
سوف
يناسب التباين إما <: T أو>: T.
في TypeScript ، يكون كل نوع معقد متغيرًا في أعضائه - الكائنات والفئات والمصفوفات وأنواع إرجاع الوظائف - مع استثناء واحد: أنواع معلمات الوظائف متناقضة.
. , . ( ). , Scala, Kotlin Flow, , .
TypeScript : , , , (, id deleteUser, , , ).
تباين الوظيفة
دعنا نفكر في بعض الأمثلة.
الوظيفة A هي نوع فرعي من الوظيفة B إذا كان A له نفس أو أقل (عدد المعلمات) من B ، و:
- النوع هذا ، الذي ينتمي إلى A ، إما غير محدد ، أو>: من النوع هذا ، ينتمي إلى B.
- كل من المعلمات A>: المعامل المقابل في B.
- نوع الإرجاع A <: نوع الإرجاع B.
لاحظ أنه لكي تكون الوظيفة A نوعًا فرعيًا من الوظيفة B ، يجب أن يكون هذا النوع والمعلمات>: النظراء في B ، بينما يجب أن يكون نوع الإرجاع <:. لماذا ينعكس الشرط؟ لماذا لا يعمل شرط <: البسيط لكل مكون (من النوع هذا ، وأنواع المعلمات ، ونوع الإرجاع) ، كما هو الحال مع الكائنات ، والمصفوفات ، والنقابات ، وما إلى ذلك؟
لنبدأ بتحديد ثلاثة أنواع (بدلاً من الفئة ، يمكنك استخدام أنواع أخرى ، حيث A: <B <: C):
class Animal {}
class Bird extends Animal {
chirp() {}
}
class Crow extends Bird {
caw() {}
}
So Crow <: Bird <: Animal.
دعنا نحدد وظيفة تأخذ Bird وتجعلها تغرد:
function chirp(bird: Bird): Bird {
bird.chirp()
return bird
}
حتى الان جيدة جدا. ما الذي يسمح لك TypeScript بالتوجيه إلى الزقزقة؟
chirp(new Animal) // TS2345: 'Animal'
chirp(new Bird) // 'Bird'.
chirp(new Crow)
مثال الطيور (كمعامل غرد لنوع الطيور) أو مثال كرو (كنوع فرعي من الطيور). نوع فرعي يمر يعمل كما هو متوقع.
لنقم بإنشاء وظيفة جديدة. هذه المرة ستكون المعلمة دالة:
function clone(f: (b: Bird) => Bird): void {
// ...
}
يتطلب الاستنساخ وظيفة f التي تأخذ Bird وتعيد Bird. ما أنواع الوظائف التي يمكن تمريرها إلى f بأمان؟ من الواضح أن الوظيفة التي تستقبل الطيور وتعيدها:
function birdToBird(b: Bird): Bird {
// ...
}
clone(birdToBird) // OK
ماذا عن الوظيفة التي تأخذ طائرًا لكنها ترجع غرابًا أو حيوانًا؟
function birdToCrow(d: Bird): Crow {
// ...
}
clone(birdToCrow) // OK
function birdToAnimal(d: Bird): Animal {
// ...
}
clone(birdToAnimal) // TS2345: '(d: Bird) =>
// Animal'
// '(b: Bird) => Bird'. 'Animal'
// 'Bird'.
يعمل BirdToCrow كما هو متوقع ، لكن birdToAnimal يرمي خطأ. لماذا ا؟ تخيل أن تطبيق استنساخ يبدو كالتالي:
function clone(f: (b: Bird) => Bird): void {
let parent = new Bird
let babyBird = f(parent)
babyBird.chirp()
}
بتمرير الدالة f للاستنساخ ، والتي تُعيد Animal ، لا يمكننا استدعاء chirp. فيها. لذلك ، يجب أن تتأكد TypeScript من أن الوظيفة التي نمررها ترجع على الأقل Bird.
عندما نقول أن الدوال متغايرة في أنواع الإرجاع الخاصة بها ، فهذا يعني أن الوظيفة يمكن أن تكون نوعًا فرعيًا لدالة أخرى فقط إذا كان نوع الإرجاع <: نوع الإرجاع لتلك الوظيفة.
حسنًا ، ماذا عن أنواع المعلمات؟
function animalToBird(a: Animal): Bird {
// ...
}
clone(animalToBird) // OK
function crowToBird(c: Crow): Bird {
// ...
}
clone(crowToBird) // TS2345: '(c: Crow) =>
// Bird' '
// (b: Bird) => Bird'.
لكي تكون الوظيفة متوافقة مع وظيفة أخرى ، يجب أن تكون جميع أنواع معلماتها (بما في ذلك هذا)>: المعلمات المقابلة لها في الوظيفة الأخرى. لفهم السبب ، فكر في كيفية قيام المستخدم بتنفيذ CrowToBird قبل تمريره للاستنساخ؟
function crowToBird(c: Crow): Bird {
c.caw()
return new Bird
}
TSC-: STRICTFUNCTIONTYPES
- TypeScript this. , , {«strictFunctionTypes»: true} tsconfig.json.
{«strict»: true}, .
الآن ، إذا كان الاستنساخ يستدعي كروتوبيرد مع طائر جديد ، فسنحصل على استثناء ، لأن .caw مُعرَّف في جميع الطيور ولكن ليس في كل الطيور.
هذا يعني أن الوظائف متناقضة في معاملاتها وهذا النوع. أي أن الوظيفة يمكن أن تكون نوعًا فرعيًا لوظيفة أخرى فقط إذا كانت كل من معلماتها واكتبها>: المعلمات المقابلة لها في الوظيفة الأخرى.
لحسن الحظ ، لا يلزم حفظ هذه القواعد. فقط تذكرها عندما يعطي المحرر تسطيرًا أحمر عند تمرير وظيفة مكتوبة بشكل غير صحيح في مكان ما.
التوافق
تعتبر العلاقات بين النوع الفرعي والنوع العلوي مفهومًا رئيسيًا في أي لغة مكتوبة بشكل ثابت. كما أنها مهمة لفهم كيفية عمل التوافق (تذكر أن التوافق يشير إلى قواعد TypeScript التي تحكم استخدام النوع A حيث يكون النوع B مطلوبًا).
عندما يحتاج TypeScript للإجابة على السؤال ، "هل النوع A متوافق مع النوع B؟" ، فإنه يتبع قواعد بسيطة. بالنسبة للأنواع غير التعدادية - مثل المصفوفات والمنطقية والأرقام والكائنات والوظائف والفئات ومثيلات الفئة والسلاسل ، بما في ذلك الأنواع الحرفية - يكون A متوافقًا مع B إذا كان أحد الشروط صحيحًا.
- أ <: ب.
- أ هو أي.
القاعدة 1 هي مجرد تعريف من النوع الفرعي: إذا كان A هو نوع فرعي من B ، فعندئذٍ حيثما تكون هناك حاجة إلى B ، يمكنك استخدام A.
القاعدة 2 هي استثناء للقاعدة 1 لسهولة التفاعل مع كود JavaScript.
بالنسبة لأنواع التعداد التي تم إنشاؤها بواسطة الكلمات الأساسية للتعداد أو التعداد الثابت ، يكون النوع A متوافقًا مع التعداد B إذا كان أحد الشروط صحيحًا.
- أ عضو في العد ب.
- لدى B عضو واحد على الأقل من النوع رقم ، و A هو رقم.
القاعدة 1 هي نفسها تمامًا للأنواع البسيطة (إذا كان A عضوًا في B ، فإن A من النوع B ونقول B <: B).
تعتبر القاعدة 2 ضرورية لتسهيل العمل مع التعدادات ، والتي تعرض أمان TypeScript للخطر بشكل خطير (راجع القسم الفرعي "Enum" في الصفحة 60) ، وأوصي بتجنبها.
توسيع النوع
يعد توسيع النوع هو المفتاح لفهم كيفية عمل استدلال النوع. TypeScript متساهل في التنفيذ ومن المرجح أن يخطئ في استنتاج نوع أكثر عمومية من استنتاج أكثر دقة ممكنة. هذا سيجعل حياتك أسهل ويقلل الوقت الذي تستغرقه للتعامل مع نوع ملاحظات المدقق.
لقد رأيت بالفعل عدة أمثلة لتوسيع النوع في الفصل 3. ضع في اعتبارك الآخرين.
عندما تعلن متغيرًا على أنه قابل للتغيير (مع let أو var) ، فإن نوعه يتوسع من نوع القيمة الخاص به إلى النوع الأساسي الذي ينتمي إليه الحرف:
let a = 'x' // string
let b = 3 // number
var c = true // boolean
const d = {x: 3} // {x: number}
enum E {X, Y, Z}
let e = E.X // E
لا ينطبق هذا على الإعلانات غير القابلة للتغيير:
const a = 'x' // 'x'
const b = 3 // 3
const c = true // true
enum E {X, Y, Z}
const e = E.X // E.X
يمكنك استخدام نوع التعليق التوضيحي الصريح لمنعه من التوسع:
let a: 'x' = 'x' // 'x'
let b: 3 = 3 // 3
var c: true = true // true
const d: {x: 3} = {x: 3} // {x: 3}
عندما تعيد تعيين نوع غير ممتد باستخدام let أو var ، فإن TypeScript يمدها لك. لمنع هذا ، أضف تعليقًا توضيحيًا صريحًا للنوع إلى الإعلان الأصلي:
const a = 'x' // 'x'
let b = a // string
const c: 'x' = 'x' // 'x'
let d = c // 'x'
المتغيرات التي تمت تهيئتها إلى قيمة خالية أو غير محددة قم بالتوسيع إلى أي:
let a = null // any
a = 3 // any
a = 'b' // any
ولكن ، عندما تتم تهيئة متغير إلى قيمة خالية أو غير معرف ، يترك النطاق الذي تم التصريح به فيه ، يقوم TypeScript بتعيين نوع معين له:
function x() {
let a = null // any
a = 3 // any
a = 'b' // any
return a
}
x() // string
النوع const يساعد النوع const على تجنب تمديد تعريف النوع. استخدمه كتأكيد نوع (راجع القسم الفرعي "موافقات النوع" في الصفحة 185):
let a = {x: 3} // {x: number}
let b: {x: 3} // {x: 3}
let c = {x: 3} as const // {readonly x: 3}
يستبعد const توسع النوع ويميز بشكل متكرر أعضائه على أنهم للقراءة فقط ، حتى في هياكل البيانات المتداخلة بعمق:
let d = [1, {x: 2}] // (number | {x: number})[]
let e = [1, {x: 2}] as const // readonly [1, {readonly x: 2}]
استخدمه كعنصر ثابت عندما تريد أن يستنتج TypeScript أضيق ما يمكن.
يتم أيضًا تشغيل Checking For Extra Properties
Type عندما يتحقق TypeScript مما إذا كان أحد أنواع الكائنات متوافقًا مع نوع آخر من الكائنات.
تعد أنواع الكائنات متغيرة في أعضائها (راجع القسم الفرعي "تنوع الشكل والصفيف" في الصفحة 148). ولكن ، إذا اتبعت TypeScript هذه القاعدة بدون فحوصات إضافية ، فقد تظهر مشكلات.
على سبيل المثال ، ضع في اعتبارك كائن خيارات يمكنك تمريره إلى فصل دراسي لتخصيصه:
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options) {}
}
new API({
baseURL: 'https://api.mysite.com',
tier: 'prod'
})
ماذا يحدث الآن إذا أخطأت في الخيار؟
new API({
baseURL: 'https://api.mysite.com',
tierr: 'prod' // TS2345: '{tierr: string}'
}) // 'Options'.
//
// , 'tierr'
// 'Options'. 'tier'?
هذا خطأ شائع في JavaScript ، ومن الجيد أن يساعدك TypeScript في اكتشافه. ولكن إذا كانت أنواع الكائنات متغايرة في أعضائها ، فكيف يعترضها TypeScript؟
بعبارات أخرى:
- توقعنا النوع {baseURL: string، cacheSize؟: number، tier؟: 'prod' | "dev"}.
- مررنا النوع {baseURL: string، tierr: string}.
- النوع الذي تم تمريره هو نوع فرعي من النوع المتوقع ، لكن TypeScript يعرف أنه يقوم بالإبلاغ عن خطأ.
من خلال التحقق من الخصائص الإضافية ، عندما تحاول تعيين نوع حرفي كائن جديد T إلى نوع آخر ، U ، و T له خصائص لا تمتلكها U ، يبلغ TypeScript عن خطأ. النوع الحرفي للكائن
الجديد هو النوع الذي استنتجه TypeScript من كائن حرفي. إذا كان هذا الكائن يستخدم تأكيد نوع (راجع القسم الفرعي "تأكيدات النوع" في الصفحة 185) أو تم تعيينه إلى متغير ، فسيتم توسيع النوع الجديد إلى نوع الكائن العادي ويفقد بدته. دعنا نحاول جعل هذا التعريف أكثر اتساعًا:
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options) {}
}
new API({ ❶
baseURL: 'https://api.mysite.com',
tier: 'prod'
})
new API({ ❷
baseURL: 'https://api.mysite.com',
badTier: 'prod' // TS2345: '{baseURL:
}) // string; badTier: string}'
// 'Options'.
new API({ ❸
baseURL: 'https://api.mysite.com',
badTier: 'prod'
} as Options)
let badOptions = { ❹
baseURL: 'https://api.mysite.com',
badTier: 'prod'
}
new API(badOptions)
let options: Options = { ❺
baseURL: 'https://api.mysite.com',
badTier: 'prod' // TS2322: '{baseURL: string;
} // badTier: string}'
// 'Options'.
new API(options)
❶ إنشاء مثيل لواجهة برمجة التطبيقات باستخدام baseURL وإحدى خاصيتين اختياريتين: الطبقة. كل شيء يعمل.
❷ نكتب الطبقة عن طريق الخطأ على أنها badTier. كائن الخيارات الذي نمرره إلى واجهة برمجة التطبيقات الجديدة جديد (يتم استنتاج نوعه ، وهو غير متوافق مع المتغير ، ولا نكتب تأكيدات له) ، لذلك عند التحقق من الخصائص غير الضرورية ، يكتشف TypeScript خاصية badTier إضافية (والتي يتم تحديدها في كائن الخيارات ، ولكن ليس في خيارات النوع).
❸ اذكر أن كائن الخيارات غير الصالح من النوع Options. لم يعد TypeScript يعرضه على أنه جديد وينتهي من فحص الخصائص الإضافية إلى عدم وجود أخطاء. تم وصف بناء الجملة بصيغة T في قسم "تأكيدات النوع" على الصفحة. 185.
❹ تعيين كائن الخيارات إلى متغير badOptions. لم يعد TypeScript يعتبره جديدًا ، وبعد التحقق من الخصائص غير الضرورية ، استنتج أنه لا توجد أخطاء.
❺ عندما نكتب الخيارات بشكل صريح كخيارات ، يكون الكائن الذي نعينه للخيارات جديدًا ، لذا يتحقق TypeScript من الخصائص الإضافية ويعثر على خطأ. لاحظ أنه في هذه الحالة لا يتم التحقق من الخصائص الإضافية عندما نمرر الخيارات إلى واجهة برمجة التطبيقات الجديدة ، ولكنه يحدث عندما نحاول تعيين كائن خيارات إلى متغير الخيارات.
لست بحاجة إلى حفظ هذه القواعد. هذا مجرد دليل داخلي من TypeScript للقبض على أكبر عدد ممكن من الأخطاء. فقط ضعهم في اعتبارك إذا تساءلت فجأة عن كيفية اكتشاف TypeScript لخلل لم يلاحظه حتى Ivan - جهاز ضبط الوقت القديم لشركتك وأيضًا مراقب كود احترافي. ينفذ
تحسين
TypeScript تنفيذًا رمزيًا لاستدلال الكتابة. تستخدم وحدة فحص النوع إرشادات تدفق الأوامر (مثل if ، و ، ، و || ، والتبديل) جنبًا إلى جنب مع استعلامات النوع (مثل typeof ، و exampleof ، و in) ، وبالتالي تأهيل الأنواع كمبرمج يقرأ من خلال التعليمات البرمجية. ومع ذلك ، يتم دعم هذه الميزة المفيدة بلغات قليلة جدًا.
تخيل أنك طورت واجهة برمجة تطبيقات لتعريف قواعد CSS في TypeScript ، ويريد زميلك استخدامها لتعيين عرض عنصر HTML. يمر العرض الذي تريد تحليله والتحقق منه لاحقًا.
أولاً ، دعنا ننفذ دالة لتحليل سلسلة CSS إلى قيمة ووحدة:
//
// , CSS
type Unit = 'cm' | 'px' | '%'
//
let units: Unit[] = ['cm', 'px', '%']
// . . null,
function parseUnit(value: string): Unit | null {
for (let i = 0; i < units.length; i++) {
if (value.endsWith(units[i])) {
return units[i]
}
}
return null
}
ثم نستخدم parseUnit لتحليل العرض المقدم من المستخدم. يمكن أن يكون العرض رقمًا (ربما بالبكسل) ، أو سلسلة مرفقة بالوحدات ، أو فارغة ، أو غير محددة.
في هذا المثال ، نستخدم تأهيل النوع عدة مرات:
type Width = {
unit: Unit,
value: number
}
function parseWidth(width: number | string | null |
undefined): Width | null {
// width — null undefined, .
if (width == null) { ❶
return null
}
// width — number, .
if (typeof width === 'number') { ❷
return {unit: 'px', value: width}
}
// width.
let unit = parseUnit(width)
if (unit) { ❸
return {unit, value: parseFloat(width)}
}
// null.
return null ❹
}
❶ يستطيع TypeScript فهم أن المساواة الفضفاضة لجافا سكريبت مقابل القيمة الخالية ستعود صحيحًا لكل من القيم الخالية وغير المعرفة. وهو يعلم أيضًا أنه إذا نجح الشيك ، فسنقوم بإرجاعه ، ولكن إذا لم نقم بإرجاع ، ففشل الشيك ومنذ تلك اللحظة فصاعدًا ، يكون نوع العرض هو رقم | سلسلة (لم يعد من الممكن أن تكون فارغة أو غير محددة). نقول أن النوع تم تنقيته من رقم | سلسلة | فارغ | غير محدد في العدد | خيط.
❷ يسأل نوع الفحص عن قيمة في وقت التشغيل لمعرفة نوعه. تستفيد TypeScript أيضًا من typeof في وقت الترجمة: في فرع if حيث يمر الاختبار ، يعرف TypeScript أن العرض عبارة عن رقم. خلاف ذلك (إذا كان هذا الفرع يعود) يجب أن يكون العرض سلسلة - النوع الوحيد المتبقي.
❸ نظرًا لأن parseUnit يمكن أن ترجع قيمة خالية ، فإننا نتحقق من ذلك. يعرف TypeScript أنه إذا كانت الوحدة صحيحة ، فيجب أن تكون من النوع Unit في فرع if. وإلا ، فإن الوحدة غير صالحة ، مما يعني أن نوعها لاغية (مصقولة من الوحدة | فارغة).
❹ أخيرًا ، نعود فارغًا. يمكن أن يحدث هذا فقط إذا قام المستخدم بتمرير سلسلة للعرض ، لكن هذه السلسلة تحتوي على وحدات غير مدعومة.
مررت بتدريب فكري TypeScript لكل نوع تنقيح تم إجراؤه. يقوم TypeScript بعمل رائع في أخذ تفكيرك أثناء قراءة وكتابة التعليمات البرمجية وبلورتها في فحص النوع وترتيب الاستدلال.
أنواع الانضمام المميزة
كما اكتشفنا للتو ، يتمتع TypeScript بفهم جيد لكيفية عمل JavaScript وهو قادر على تتبع مؤهلات النوع لدينا كما لو كان يقرأ عقولنا.
لنفترض أننا نقوم بإنشاء نظام حدث مخصص لأحد التطبيقات. نبدأ بتحديد أنواع الأحداث جنبًا إلى جنب مع الوظائف التي تتعامل مع وصول تلك الأحداث. تخيل أن UserTextEvent يحاكي حدث لوحة المفاتيح (على سبيل المثال ، كتب المستخدم النص <input />) ، ويحاكي UserMouseEvent حدث الماوس (قام المستخدم بتحريك الماوس عند الإحداثيات [100 ، 200]):
type UserTextEvent = {value: string}
type UserMouseEvent = {value: [number, number]}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (typeof event.value === 'string') {
event.value // string
// ...
return
}
event.value // [number, number]
}
يعرف TypeScript أنه داخل كتلة if ، يجب أن يكون event.value سلسلة (بفضل نوع التحقق) ، أي event.value بعد كتلة if يجب أن تكون [number، number] tuple (بسبب الإرجاع في كتلة if).
إلى أين ستقود المضاعفات؟ دعنا نضيف توضيحات لأنواع الأحداث:
type UserTextEvent = {value: string, target: HTMLInputElement}
type UserMouseEvent = {value: [number, number], target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (typeof event.value === 'string') {
event.value // string
event.target // HTMLInputElement | HTMLElement (!!!)
// ...
return
}
event.value // [number, number]
event.target // HTMLInputElement | HTMLElement (!!!)
}
أثناء عمل التصفية لـ event.value ، لم تكن مع event.target. لماذا ا؟ عندما يتلقى المؤشر معلمة من النوع UserEvent ، فهذا لا يعني أنك بحاجة إلى تمرير إما UserTextEvent أو UserMouseEvent إليه - في الواقع ، يمكنك تمرير وسيطة من النوع UserMouseEvent | UserTextEvent. وبما أن أعضاء الاتحاد يمكن أن يتداخلوا ، فإن TypeScript يحتاج إلى طريقة أكثر موثوقية لمعرفة متى وأية حالة الاتحاد ذات صلة.
يمكنك القيام بذلك باستخدام الأنواع الحرفية وتعريف العلامة لكل حالة من نوع الاتحاد. علامة لطيفة:
- في كل حالة ، يقع في نفس مكان نوع الاتحاد. يتضمن نفس حقل الكائن عندما يتعلق الأمر بدمج أنواع الكائنات ، أو نفس الفهرس عندما يتعلق الأمر بدمج المجموعات. في الممارسة العملية ، النقابات التي تتعرض للتمييز هي في الغالب أشياء.
- تكتب كنوع حرفي (سلسلة حرفية ، رقمية ، منطقية ، إلخ). يمكنك مزج أنواع مختلفة من المعطيات الحرفية ومطابقتها ، لكن من الأفضل الالتزام بنوع واحد. عادةً ما يكون هذا نوعًا من السلسلة الحرفية.
- ليست عالمية. يجب ألا تتلقى العلامات وسيطات من النوع العام.
- متبادل (فريد داخل نوع الاتحاد).
لنقوم بتحديث أنواع الأحداث مع مراعاة ما سبق:
type UserTextEvent = {type: 'TextEvent', value: string,
target: HTMLInputElement}
type UserMouseEvent = {type: 'MouseEvent', value: [number, number],
target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (event.type === 'TextEvent') {
event.value // string
event.target // HTMLInputElement
// ...
return
}
event.value // [number, number]
event.target // HTMLElement
}
الآن ، عندما نقوم بتنقية الحدث استنادًا إلى قيمة الحقل الذي تم وضع علامة عليه (event.type) ، يعرف TypeScript أنه يجب أن يكون هناك UserTextEvent في فرع if ، وبعد فرع if ، يجب أن يكون UserMouseEvent. نظرًا لأن العلامات فريدة في كل نوع اتحاد ، يعرف TypeScript أنها متعارضة.
استخدم الصلات المميزة عند كتابة دالة تعالج حالات مختلفة من نوع الصلة. على سبيل المثال ، عند العمل مع إجراءات Flux ، أو عمليات الاستعادة ، أو useReducer في React.
يمكنك التعرف على الكتاب بمزيد من التفصيل والطلب المسبق بسعر خاص على موقع الناشر