הצטמצמות - למה TypeScript לא מצמצם סוגי מערכים?


למה זה ש-TypeScript לא מצמצם את סוג המערכים?

function test(input: (string | number)[]): string[] {
  // The type of .map(...) reports that it returns string[].
  input = input.map(x => x.toString())
  // Type error: Type '(string | number)[]' is not assignable to type 'string[]'.
  return input
}

הפתרון המעוקף אינו תלוי בכך שהוא מצמצם את הטיפוס רק על ידי שימוש מיידי או הקצאה למשתנה חדש:

function test(input: (string | number)[]): string[] {
  return input.map(x => x.toString())
}

function test(input: (string | number)[]): string[] {
  const newInput = input.map(x => x.toString())
  return newInput
}

ניסיתי להמיר טיפוס, אבל במבט לאחור זה כמובן עובד רק על שימוש, לדוגמה return input as string[], ולא יצמצם את הטיפוס, כי .map(...) כבר מחזיר את הטיפוס המצומצם כראוי.

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

בדקתי את התיעוד הרשמי ובחנתי שאלות דומות ב-Stack Overflow, אבל אלא אם פספסתי משהו, לא ראיתי שהשאלה הספציפית הזו נענתה במשהו אחר מלבד פשוט להקצות מחדש.

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

> tsc --version                                                                                                                                                                                                                                                                              
Version 4.2.3

תשובה #1

טוב או רע, הצמצום בהתבסס על השמה או הצמצום הכללי באמצעות ניתוח זרימת שליטה (כמו שמיושם ב-microsoft/TypeScript#8010) קורה רק כאשר המשתנים המעורבים הם מסוגי איחוד. ובמונח סוג איחוד אני מתכוון למצב שבו הסוג עצמו הוא איחוד ישיר, כמו {a: string} | {a: number} או Array<string> | Array<number>. סוג אובייקט יחיד עם תכונות מסוגי איחוד כמו {a: string | number} אינו בעצמו איחוד; וכן גם interface גנרי המוגדר עם פרמטר סוגי איחוד כמו Array<string | number>. קיימת הצעה ותיקה ב-microsoft/TypeScript#16976 לתמיכה בצמצום זרימת שליטה שאינו מבוסס איחוד, אך אין אינדיקציה מתי או אם זה ייושם אי פעם. לכן input=input.map(x => x.toString()) לא ישנה את סוג ההופעה של input.

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

תשובה #2

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

function isStringArray(x: any[]): x is string[] {
  return x.every(i => typeof i === "string");
}

function test(input: (string | number)[]): string[] {
  // The type of .map(...) reports that it returns string[].
  input = input.map(x => x.toString())
  if (isStringArray(input)) {
    return input;   
  }
  return [];
}

זה עובד כי אנחנו משתמשים במגונן סוג typeof בתוך תחזית סוג שמשתמשים בה בתוך if.

אז אם כל אלמנט במערך הוא מחרוזת, החזר "true" שמשמעותו שהמערך שהוזן הוא string[]. כאשר משתמשים בזה ב-if, הנתיב של "true" מצמצם את הסוג של input ל-string[] ואנו יכולים להחזיר אותו בהצלחה.

אז בעוד שאתה חושב שהחזרה ישירה או הקצאה ראשונה למשתנה חדש היא "פתרון חלופי", אתה יכול לראות כאן ששימוש בצמצום TypeScript כאן הוא למעשה יותר קוד אם אתה מתעקש להשתמש מחדש ב-input מאשר האלטרנטיבות שמשתמשות בטיפוס המולד של ה-.map המוחזר.

מגרש המשחקים של TypeScript