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
void | never | |
|---|---|---|
| הפונקציה מסתיימת | כן | לא (זורקת שגיאה או רצה לנצח) |
| ערך בזמן ריצה | 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; הוסיפו הגדרת טיפוס או תקנו את הטיפוסים הסותרים.