Menu

סימן קריאה ב-TypeScript: ה-Non-Null Assertion (!)

סימן קריאה אחרי ערך, כמו user!, הוא האופרטור non-null assertion: הוא מסיר את null ו-undefined מהטיפוס בלי שום בדיקה בזמן ריצה. כאן תלמדו מה x! עושה, את צורות ה-definite assignment let x!: T ו-prop!: T, למה הן מסוכנות, וחלופות בטוחות יותר.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

סימן קריאה אחרי ביטוי, value!, הוא האופרטור non-null assertion. הוא מסיר את null ו-undefined מהטיפוס, כך שאפשר להשתמש ב-number | undefined כ-number. זו הבטחה לקומפיילר, לא בדיקה: בזמן ריצה לא קורה כלום.

בלי ה-!, tea.toFixed(2) היא שגיאה TS18048, 'tea' is possibly 'undefined'. איתו, הקוד מתקמפל כי אמרתם לקומפיילר שהמפתח קיים.

מה x! עושה בזמן ריצה: כלום

ה-! נמחק מהפלט. ה-JavaScript המקומפל של prices.get("coffee")! הוא פשוט prices.get("coffee"). אם הטענה שגויה, השגיאה מופיעה מאוחר יותר, במקום הראשון שבו משתמשים בערך החסר:

התוכנית מדפיסה Cannot read properties of undefined (reading 'toFixed'). הקריסה קורית בשורה שאחרי ה-!, ובקוד אמיתי היא יכולה להיות רחוקה בהרבה: ה-undefined עשוי להישמר באובייקט ולהתפוצץ בקובץ אחר. המרחק הזה הוא הסיבה ש-! מסוכן.

Definite assignment: let x!: T

לאותו סימן בהצהרה יש משמעות קרובה. TypeScript עוקב אחרי השאלה אם משתנה קיבל ערך לפני שקוראים אותו, והוא לא יכול לעקוב אחרי השמה שנעשית בתוך פונקציה אחרת:

index.ts(9,13): error TS2454: Variable 'config' is used before being assigned.

let config!: { port: number }; הוא definite assignment assertion: "זה יקבל ערך לפני כל קריאה". זה מתקן את השגיאה, עם אותו מלכוד כמו x!: אם init() ידולג אי פעם, הקריאה תקבל undefined. בדרך כלל עדיף לשנות את מבנה הקוד, למשל const config = init(); כש-init מחזירה את האובייקט.

מאפייני מחלקה: prop!: T

עם strictPropertyInitialization (חלק מ-strict), כל מאפיין מחלקה חייב לקבל ערך בהצהרה שלו או ב-constructor. אחרת מקבלים TS2564: Property 'socket' has no initializer and is not definitely assigned in the constructor. כשמאפיין מוגדר מאוחר יותר על ידי מתודה או framework, prop!: T אומר לקומפיילר לקבל אותו:

ה-console.log הראשון מראה את הפער: הטיפוס אומר ש-socket תמיד קיים, אבל לפני open() הוא undefined. קריאה ל-c.socket.send בשלב הזה מתקמפלת וזורקת שגיאה. אם המאפיין באמת יכול להיות חסר, הצהירו עליו כ-socket?: ... ובדקו אותו, או צרו את האובייקט ב-constructor. המקום העיקרי שבו prop! הוא נוהג מקובל הוא framework שממלא מאפיין אחרי היצירה: @ViewChild(...) child!: ChildDirective של Angular (מוגדר לפני ש-ngAfterViewInit רץ), או מחלקות entity של ORM שהספרייה ממלאת את העמודות שלהן כשהיא טוענת שורה (בתיעוד של MikroORM כתוב @Property() title!: string).

חלופות בטוחות יותר

את רוב ה-! אפשר להחליף במשהו שהקומפיילר בודק, או בבדיקה שנכשלת בקול רם במקום הנכון:

שתי פונקציות העזר מצמצמות את הטיפוס כמו ש-! עושה, אבל הנחה שגויה מייצרת missing HOST במקום, במקום TypeError במקום אחר. assertDefined היא assertion function (asserts value is ...): אחרי הקריאה, הקומפיילר מתייחס ל-host כ-string. דפוסים נוספים ב-type guards.

במקוםכתבומה קורה כשהערך חסר
user!.nameif (user) { user.name }הבלוק מדולג
user!.nameuser?.nameundefined
count!count ?? 0משתמשים בערך ברירת המחדל
map.get(k)!must(map.get(k), "k")שגיאה ברורה בשורה הזו
let x!: Tconst x = compute()אין מה שישתבש

סימני הקריאה האחרים

ל-! יש משמעויות שונות לפי המיקום שלו:

קודמשמעות
value! (אחרי ביטוי)non-null assertion, רק ב-TypeScript
let x!: T, prop!: Tdefinite assignment assertion, רק ב-TypeScript
!value (לפני ביטוי)NOT לוגי, JavaScript רגיל
!!valueממיר לבוליאני, JavaScript רגיל
a !== b, a != bאי שוויון, JavaScript רגיל

רק השניים הראשונים נמחקים בזמן קומפילציה. !value ו-!!value רצים בזמן ריצה ומחזירים ערך בוליאני.

שאלות נפוצות

מה המשמעות של סימן קריאה אחרי משתנה ב-TypeScript?

value! הוא האופרטור non-null assertion. הוא אומר לקומפיילר ש-value אינו null או undefined, ולכן הטיפוס שלו מאבד את שני האיברים האלה: string | undefined הופך ל-string. הוא מוסר מה-JavaScript שנוצר ולא מוסיף שום בדיקה בזמן ריצה, כך שאם טעיתם התוכנית נכשלת מאוחר יותר עם TypeError.

מה ההבדל בין ! ל-? ב-TypeScript?

x! טוען שהערך קיים ונותן לכם את הטיפוס בלי null, בלי שום בדיקה. x?.y בודק בזמן ריצה: אם x הוא null או undefined הוא עוצר ומחזיר undefined. בהצהרה, name?: string הופך מאפיין לאופציונלי, ואילו name!: string אומר שמאפיין חובה יושם במקום שהקומפיילר לא רואה.

מה המשמעות של let x!: string?

זה definite assignment assertion. הוא אומר לקומפיילר שהמשתנה יקבל ערך לפני שיקראו אותו, למרות שהקומפיילר לא יכול להוכיח זאת (למשל, ההשמה קורית בתוך פונקציה אחרת). בלעדיו, קריאת המשתנה היא שגיאה TS2454, Variable 'x' is used before being assigned.

איך מתקנים את "has no initializer and is not definitely assigned in the constructor"?

זו שגיאה TS2564 מ-strictPropertyInitialization. תנו למאפיין ערך התחלתי, השימו לו ערך ב-constructor, הפכו אותו לאופציונלי (prop?: T), או, אם framework או מתודת init באמת מגדירים אותו לפני השימוש, כתבו prop!: T. ה-! הוא האפשרות האחרונה כי שום דבר לא בודק את ההבטחה.

האם האופרטור non-null assertion הוא הרגל רע?

הוא לא שגוי, אבל כל ! הוא טענה שלא נבדקת. הגדרות lint כמו @typescript-eslint/no-non-null-assertion מסמנות אותו. העדיפו בדיקה שמצמצמת (if (x), x ?? fallback, x?.y) או פונקציית עזר שזורקת שגיאה ברורה. השאירו את ! למקומות שבהם הערך מובטח על ידי לוגיקה שהקומפיילר לא יכול לעקוב אחריה.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל