Menu

TypeScript type narrowing: איך עובד control flow analysis

Narrowing הוא הדרך שבה TypeScript הופך טיפוס רחב כמו string | number לטיפוס ספציפי בתוך if, בתוך switch או אחרי return מוקדם. כל צורות הצמצום במקום אחד: typeof, truthiness, שוויון, in, instanceof, השמות, type predicates ו-discriminated unions.

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

Type narrowing הוא TypeScript שמחשב טיפוס ספציפי יותר לערך בנקודה מסוימת בקוד, על סמך הבדיקות שהקוד כבר ביצע. פרמטר מטיפוס string | number הופך ל-string בתוך if (typeof x === "string") ול-number ב-else.

קריאה ל-value.toFixed(2) לפני הבדיקה הייתה שגיאת קומפילציה, כי toFixed לא קיים על string. הבדיקה היא JavaScript רגיל ורצה בזמן ריצה; הצמצום הוא הקומפיילר שקורא אותה ומתאים את הטיפוס. שום דבר נוסף לא נוצר בפלט.

Control flow analysis

TypeScript עוקב אחרי כל מסלול בתוך פונקציה: if/else, return ו-throw מוקדמים, switch, לולאות, ואופרטורי הקיצור &&, ||, ?? ו-?:. בכל נקודה, הטיפוס של משתנה הוא מה שעדיין אפשרי שם.

סגנון ה-return המוקדם ("guard clauses") הוא הדרך הקריאה ביותר לצמצם: מטפלים קודם במקרים החריגים, ושאר הפונקציה עובד עם הטיפוס הנקי.

כל הדרכים לצמצם

צורהדוגמהמצמצם
typeoftypeof x === "string"טיפוסים פרימיטיביים ופונקציות
Truthinessif (x)מסיר null, undefined וליטרלים falsy
שוויוןx === "a", x == null, x !== undefinedליטרלים, null, undefined
in"swim" in petunions של אובייקטים, לפי מאפיין
instanceoferr instanceof TypeErrorמופעים של מחלקות
Array.isArrayArray.isArray(x)מערכים מול כל השאר
השמהx = 5לטיפוס שהושם
Type predicatefunction isUser(x: unknown): x is Userכל מה שאפשר לבדוק
פונקציית assertionfunction assertUser(x: unknown): asserts x is Userכל מה שבא אחרי הקריאה
מאפיין מבחיןswitch (shape.kind)unions מתויגים

שלוש האחרונות מוסברות בעמודים של type guards ושל discriminated unions. השאר מוסברות בהמשך.

צמצום לפי truthiness

if (x) מסיר null ו-undefined (וגם את הטיפוסים הליטרליים false, 0, ""). זה קצר, ויש בו מלכודת קלאסית אחת: 0 ו-"" הם falsy, ולכן ערכים תקינים מטופלים כחסרים.

עבור מספרים ומחרוזות, השוו במפורש ל-undefined או ל-null (או השתמשו ב-??). Truthiness מתאים לאובייקטים, למערכים ולפונקציות, שאף פעם אינם falsy.

צמצום לפי שוויון

===, !==, == ו-!= מצמצמים את שני הצדדים. השוואה לליטרל מצמצמת לאותו ליטרל; == null (שוויון רופף) תופס גם null וגם undefined בבדיקה אחת, וזה המקום היחיד שבו שוויון רופף הוא הסגנון המקובל.

השוואה בין שני משתנים מצמצמת את שניהם למה שיכול להיות משותף להם: אם a: string | number ו-b: string | boolean עוברים את a === b, שניהם string בתוך ה-if.

האופרטור in

"key" in obj מצמצם union של טיפוסי אובייקטים לאיברים שיש להם (או שאולי יש להם) את המאפיין הזה.

in עובד גם על unknown ברגע שיודעים שזה אובייקט: אחרי typeof v === "object" && v !== null && "id" in v, TypeScript יודע שלאובייקט v יש מאפיין id מטיפוס unknown. עבור unions שאתם מתכננים בעצמכם, מאפיין תגית משותף (kind: "fish") ברור יותר מבדיקה של מתודות: לדפוס הזה קוראים discriminated union.

צמצום לפי השמה

למשתנה יש טיפוס מוצהר, וטיפוס מצומצם שעוקב אחרי ההשמות שלו. השמה של ערך מצמצמת אותו לטיפוס של הערך, בגבולות הטיפוס המוצהר.

איפה הצמצום הולך לאיבוד

הצמצום מקומי ושמרני. כמה מצבים מאפסים אותו:

  • ביטוי אחר. בדיקה של obj.name מצמצמת את obj.name (ואת obj["name"]), אבל לא את obj[key] כש-key הוא משתנה מטיפוס string ולא ליטרל, וגם לא עותק שנוצר לפני הבדיקה.
  • Callbacks והשמה מחדש. בתוך callback, משתנה let מצומצם שומר על הצמצום רק אם לא מושם לו ערך שוב אחרי שה-callback נוצר. const או פרמטר שאף פעם לא מושם לו ערך מחדש נשארים מצומצמים.
  • בדיקות שמוסתרות בפונקציות עזר. פונקציה isString(x: unknown): boolean לא אומרת לקומפיילר כלום. תנו לה טיפוס החזרה של type predicate, x is string, וקריאות אליה יצמצמו כמו ש-typeof מצמצם.

הקומפיילר מדווח index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. ה-callback יכול לרוץ מאוחר יותר, אחרי x = undefined. מחקו את ההשמה האחרונה (או העתיקו את הערך ל-const בתוך ה-if), והקוד יתקמפל וידפיס 5 פעמיים.

את פונקציית העזר שמחזירה boolean אפשר לתקן בהצהרה על מה שהיא מוכיחה. זה בדיוק type guard:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

מאז TypeScript 5.5 הקומפיילר מסיק predicates כאלה עבור פונקציות חץ פשוטות, ולכן list.filter((x) => x !== undefined) מחזיר היום מערך בלי undefined.

שאלות נפוצות

מה זה type narrowing ב-TypeScript?

Narrowing הוא TypeScript שמדייק את הטיפוס של משתנה בתוך בלוק, על סמך בדיקה שהקוד מבצע. אחרי if (typeof x === "string"), string | number הוא רק string בתוך ה-if ורק number ב-else. הקומפיילר עוקב אחרי if, else, return, switch, &&, || ו-?: כדי לחשב את הטיפוס בכל נקודה, ולזה קוראים control flow analysis.

למה TypeScript לא מצמצם את הטיפוס שלי?

סיבות נפוצות: הבדיקה היא על ביטוי שונה מזה שבו משתמשים (בודקים obj.a, ומשתמשים ב-obj[key] עם key מטיפוס string); הערך הוא let שמושם לו ערך מחדש אחרי שנוצר callback, ולכן ה-callback מאבד את הצמצום; או שהבדיקה מוסתרת בפונקציית עזר שמחזירה boolean רגיל במקום type predicate מהצורה x is T.

האם type narrowing עובד בזמן ריצה?

הבדיקות כן: typeof, instanceof, in ו-=== הן JavaScript רגיל שרץ. הצמצום עצמו קיים רק בזמן קומפילציה. TypeScript קורא את הבדיקות שלכם בזמן ריצה ומתאים אליהן את הטיפוסים הסטטיים, ושום דבר לא נוסף ל-JavaScript שנוצר.

איך מצמצמים טיפוס unknown ב-TypeScript?

עם אותן בדיקות: typeof value === "string", Array.isArray(value), value instanceof Date, או עבור אובייקטים typeof value === "object" && value !== null && "id" in value. לבדיקות שחוזרות על עצמן כתבו פונקציית type guard עם טיפוס החזרה value is T.

איך מסננים undefined ממערך ב-TypeScript?

items.filter((x) => x !== undefined) מחזיר T[] בלי undefined מאז TypeScript 5.5, שמסיק את ה-callback כ-type predicate. בגרסאות ישנות יותר כתבו את ה-predicate בעצמכם: items.filter((x): x is T => x !== undefined).

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

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

להתחיל