סוג מותנה ב-TypeScript


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

זה הקוד בפועל:

interface IdLabel {
  id: number;
}

interface NameLabel {
  name: string;
}

type NameOrId<T extends string | number> = T extends number ? IdLabel : NameLabel;

function createLabel<T extends string | number>(idOrName: T): NameOrId<T> {
  if (typeof idOrName === "number") {
    return {
      id: idOrName
    };
  } else {
    return {
      name: idOrName
    };
  }
}

const a = createLabel(1);
const b = createLabel("something");

אתה יכול לראות את הטעות בתמונה הזו:

זוהי שגיאת הטקסט:

Type '{ id: T & number; }' is not assignable to type 'NameOrId<T>'.
Type '{ name: T; }' is not assignable to type 'NameOrId<T>'.

תשובה #1

כאשר טיפוס מותנה כמו NameOrId<T> תלוי בפרמטר טיפוס שלא נפתר כמו T בתוך גוף הפונקציה createLabel(), המהדר דוחה את הערכתו. טיפוס כזה הוא בעצם אטום עבור המהדר, ולכן בדרך כלל הוא לא מסוגל לוודא שערך ספציפי כמו {id: idOrName} ניתן להקצות לו.

כשאתה בודק typeof idOrName, ניתוח זרימת הבקרה יצמצם את סוג הערך של idOrName, אבל זה לא מצמצם את פרמטר הסוג T עצמו. אם typeof idOrName === "string", אז idOrName עכשיו ידוע כסוג string (או T & string), אבל T עדיין יכול להיות string | number, ועדיין מדובר בפרמטר סוג גנרי שטרם נפתר.

זו נקודת כאב ידועה של TypeScript. ישנה בעיה פתוחה, microsoft/TypeScript#33912, המבקשת דרך לאפשר לניתוח זרימת הבקרה לוודא התאמה להקצאה לסוגים מותנים לא פתורים, במיוחד כערך ההחזרה של פונקציה כמו שאתה רוצה כאן. עד שהבעיה הזו תטופל, תצטרך למצוא דרך לעקוף זאת.


פתרונות חלופיים :

כל פעם שיש לך מצב שבו אתה בטוח שהביטוי expr הוא מהסוג Type אך הקומפיילר לא, אתה תמיד יכול להשתמש בהצהרת סוג כדי להודיע לקומפיילר זאת: expr as Type, או, במקרים שבהם הקומפיילר רואה את הסוג של expr כבלתי קשור בכלל ל-Type, ייתכן שתצטרך לכתוב expr as any as Type או להשתמש בהצהרת ביניים אחרת.

זה ייתן לך את זה:

function createLabelAssert<T extends string | number>(idOrName: T): NameOrId<T> {
    if (typeof idOrName === "number") {
        return {
            id: idOrName
        } as unknown as NameOrId<T>;
    } else {
        return {
            name: idOrName
        } as unknown as NameOrId<T>;
    }
}

זה פותר את השגיאות. שים לב שכאשר אתה מבצע הצהרה, אתה לוקח על עצמך את האחריות לבטיחות הסוגים. אם היית משנה את הבדיקה typeof idOrName === "number" ל- typeof idOrName !== "number", עדיין לא הייתה שגיאת קומפילציה. אבל היית בלתי מרוצה בזמן הריצה.


כאשר יש לך פונקציה שהסוג המוחזר שלה לא יכול להיות מוסמך ביישום, אתה יכול לבצע את ה"מקבילה המוסרית" של טענת סוג: העמסה עם חתימת קריאה אחת. יישומי ההעמסה נבדקים בצורה רופפת יותר מאשר פונקציות רגילות, כך שאתה יכול לקבל את אותו ההתנהגות כמו טענת סוג מבלי להצהיר על כך בכל שורת return בנפרד:

// call signature
function createLabel<T extends string | number>(idOrName: T): NameOrId<T>;
// implementation
function createLabel(idOrName: string | number): NameOrId<string | number> {
    if (typeof idOrName === "number") {
        return {
            id: idOrName
        };
    } else {
        return {
            name: idOrName
        };
    }
}

חתימת הקריאה היא זהה, אך כעת חתימת המימוש היא רק מיפוי מ-string | number ל-IdLabel | NameLabel. שוב, הקומפיילר לא יתפוס את הבעיה שבה אתה בודק typeof idOrName !== "number" בטעות, לכן גם כאן צריך להיות זהיר.

קישור למזחוב קוד