Menu

הטיפוס never ב-TypeScript: בדיקות exhaustive ו-never מול void

never הוא הטיפוס שאין לו ערכים. זה טיפוס ההחזרה של פונקציות שאף פעם לא מסתיימות, הטיפוס שנשאר כש-narrowing פסל כל מקרה, והכלי שמאחורי בדיקות exhaustive ב-switch. כאן תלמדו מאיפה הוא מגיע ובמה הוא שונה מ-void.

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

never הוא הטיפוס שאין לו ערכים. פונקציה שטיפוס ההחזרה שלה הוא never אף פעם לא חוזרת כרגיל: היא זורקת שגיאה או רצה לנצח. משתנה מטיפוס never מסמן קוד שלא יכול לרוץ, וזה מה שמאפשר בדיקות exhaustive.

return fail(...) מתקמפל בפונקציה שמחזירה number כי never ניתן להשמה לכל טיפוס: ערך מטיפוס never לעולם לא באמת יגיע.

פונקציות שאף פעם לא חוזרות

שני סוגים של פונקציות לא מגיעים לסוף שלהם: אחת שתמיד זורקת שגיאה, ואחת עם לולאה אינסופית. הקומפיילר בודק את הטענה. פונקציה עם הגדרת never שאפשר להגיע לסוף שלה היא שגיאה TS2534, A function returning 'never' cannot have a reachable end point.

function fail(message: string): never {
  throw new Error(message);
}

function runForever(): never {
  while (true) {
    // poll, serve requests...
  }
}

ההסקה תלויה בתחביר. עבור function declaration שרק זורקת שגיאה מוסק טיפוס החזרה void, ואילו עבור arrow function או function expression שרק זורקת שגיאה מוסק never:

function f1() { throw new Error("x"); }       // () => void
const f2 = () => { throw new Error("x"); };   // () => never

narrowing מתייחס לקריאה כמבוי סתום רק כשלשם שנקרא יש טיפוס מפורש שמחזיר never: function declaration עם הגדרה : never, כמו fail למעלה, או משתנה עם הגדרת טיפוס, const fail: (m: string) => never = (m) => { throw new Error(m); }. never מוסק לא נחשב, וגם לא const fail = (m: string): never => ..., שבו רק לחץ יש הגדרת טיפוס ולמשתנה אין.

never מול void

voidnever
הפונקציה מסתיימתכןלא (זורקת שגיאה או רצה לנצח)
ערך בזמן ריצהundefinedאין: הקריאה אף פעם לא מייצרת ערך
קוד אחרי הקריאהבר השגהלא בר השגה
ניתן להשמה לטיפוסים אחריםרק ל-void, unknown, anyלכל טיפוס
שימוש טיפוסיcallbacks, event handlers, פונקציות עם תופעות לוואיfail(), assertNever(), לולאות אינסופיות

ההבדל המעשי מתגלה ב-narrowing. אחרי if (!user) fail("no user"), הקומפיילר יודע ש-user מוגדר בשורה הבאה רק אם fail מחזירה never. עם החזרת void הוא מניח שהריצה יכולה להמשיך.

בדיקות exhaustive עם never

כל case של switch על union מצמצם את הערך. כשכל איבר טופל, מה שנשאר ב-default הוא never. השמה שלו למשתנה never הופכת את "טיפלתי בכל מקרה" למשהו שהקומפיילר מאמת:

עכשיו הוסיפו איבר שלישי ל-union בלי להוסיף case:

index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.

השגיאה מציינת את האיבר ששכחתם. הוסיפו case "triangle": return (shape.base * shape.height) / 2; והקוד יתקמפל שוב. פונקציית העזר assertNever היא הצורה הרב פעמית של אותה בדיקה, וה-throw שלה עדיין חשוב בזמן ריצה: נתונים מ-JSON או מלקוח ישן יותר יכולים להכיל kind שהטיפוסים אומרים שהוא בלתי אפשרי. הדפוס הזה הוא עמוד השדרה של discriminated unions.

Narrowing עד never

אותו דבר קורה בכל narrowing, לא רק ב-switch. ברגע שכל אפשרות נפסלה, למשתנה יש טיפוס never:

אם בהמשך תרחיבו את הפרמטר ל-string | number | boolean | bigint, השורה const nothing: never = x תהפוך לשגיאה, ותצביע על הפונקציה שצריך לעדכן.

never נעלם ב-unions

never הוא הקבוצה הריקה של ערכים, ולכן הוספה שלו ל-union לא משנה כלום: string | never הוא פשוט string. כך conditional types מסננים unions. ענף שמחזיר never מסיר את האיבר הזה:

ה-Exclude<T, U> וה-Extract<T, U> המובנים עובדים בדיוק כך. ב-intersection זה ההפך: string & never הוא never.

טיפוסים בלתי אפשריים הופכים ל-never

intersection ששום ערך לא יכול לקיים מצטמצם ל-never:

type A = string & number;                   // never
type B = { kind: "a" } & { kind: "b" };     // never

קריאת מאפיין של ערך B מדווחת את הסיבה: Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents. כשטיפוס שבניתם מתברר כ-never, חפשו שני חלקים שסותרים זה את זה.

never, unknown ו-any

טיפוסערכים שהוא מכילניתן להשמה ל-מקבל
unknownכל ערךרק unknown ו-anyהכול
anyכל ערךהכול חוץ מ-neverהכול
neverאין ערכיםהכולרק never

unknown נמצא בראש היררכיית הטיפוסים ו-never בתחתית. any בכלל לא חלק מההיררכיה: הוא מכבה את הבדיקות.

שאלות נפוצות

מה זה הטיפוס never ב-TypeScript?

never הוא הטיפוס שאין לו ערכים. אי אפשר להשים לו כלום (חוץ מ-never אחר), והוא ניתן להשמה לכל טיפוס. הוא מופיע כטיפוס ההחזרה של פונקציות שתמיד זורקות שגיאה או רצות לנצח, כטיפוס של משתנה אחרי ש-narrowing פסל כל אפשרות, וכתוצאה של טיפוסים בלתי אפשריים כמו string & number.

מה ההבדל בין never ל-void?

פונקציה שמחזירה void מסתיימת כרגיל; היא פשוט לא מחזירה ערך שימושי (בזמן ריצה היא מחזירה undefined). פונקציה שמחזירה never לא מסתיימת בכלל: היא זורקת שגיאה או רצה לנצח. קוד אחרי קריאה לפונקציית never אינו בר השגה, ו-TypeScript מתייחס אליו כך בזמן narrowing.

איך עושים בדיקת exhaustive ב-TypeScript?

בענף default של switch על union, השימו את הערך למשתנה מטיפוס never, או העבירו אותו לפונקציה assertNever(value: never): never שזורקת שגיאה. אם כל מקרה מטופל, הערך שם הוא never והקוד מתקמפל. אם מקרה חסר, הקומפיילר מדווח שהאיבר החסר אינו ניתן להשמה ל-never.

למה הטיפוס שלי הוא never?

בדרך כלל כי TypeScript צמצם את כל האפשרויות (למשל אחרי בדיקה של typeof x === "string" ו-typeof x === "number" על string | number), או כי intersection בלתי אפשרי: string & number, או שני טיפוסי אובייקט שלמאפיין המשותף שלהם יש טיפוסי literal סותרים. העבירו את העכבר מעל הטיפוס בעורך כדי לראות איזה שלב יצר אותו.

מה המשמעות של "is not assignable to type never"?

הקוד ניסה לשים ערך אמיתי במקום שבו מותר רק never. בבדיקת exhaustive זה אומר שאיבר ב-union לא טופל. במקומות אחרים זה אומר לעיתים קרובות שמערך הוסק כ-never[] או ש-intersection קרס ל-never; הוסיפו הגדרת טיפוס או תקנו את הטיפוסים הסותרים.

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

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

להתחיל