TypeScript: סוג 'string' לא יכול לשמש לאינדוקס של סוג "{ [key: string]: any; }"
אני יוצר פונקציה שמוסיפה תכונה נוספת לכל איבר במערך:
type AnyObj = { [key: string]: any };
function addIndexProp<T extends AnyObj>(
obj: T[],
myProp: string
): T[] {
return obj.map(item => {
item[myProp] = 'myProp';
return item;
});
}
אבל קיבלתי את השגיאה הבאה:
tsc --noEmit src/test.ts
src/test.ts:8:5 - error TS2536: Type 'string' cannot be used to index type 'T'.
8 item[myProp] = 'myProp';
~~~~~~~~~~~~
Found 1 error.
error Command failed with exit code 1
אני לא מבין למה זה קורה כי כבר ציינתי את T באמצעות סוג אינדקס מחרוזת, ובמיוחד שהקוד הבא עובד היטב:
const test: { [key: string]: any } = { test: 1 };
const myProp = 'prop';
test[myProp] = 'myProp';
מישהו יודע למה הקטע הקוד הראשון גורם לשגיאה, ואיך ניתן לעקוף את הבעיה? תודה!
תשובה #1
הבעיה עם T extends AnyObj היא ש-T יכול להיות כל דבר שניתן להקצות לו ערך של AnyObj, ו-TypeScript תיתן חתימות אינדקס משתמעות לסוגי אובייקט מילולי של תכונות שתואמות את תכונת חתימת האינדקס. לכן סוג כמו {a: number} ניתן להקצות ל-AnyObj, למרות שאין לו חתימת אינדקס. לכן ניתן לכתוב את הקוד הבא ללא שגיאה:
let obj = { a: 123, b: 345 };
const addedProps = addIndexProp([obj], "a");
addedProps[0].a.toFixed(); // okay at compile time, error at runtime
בתוך addIndexProp(), השורה הבעייתית בסופו של דבר מקצה ערך "myProp" למאפיין שאמור להיות מסוג number. וזה לא טוב.
התיקון הנכון באמת תלוי במקרי השימוש שלך. אם אתה רק רוצה לגרום למקומלר להיות מרוצה מבלי למעשה להפוך את הקוד שלך לבטוח יותר, אתה יכול להשתמש בהצהרת סוג. זה רק יאפשר לך להקצות item[myProp]="myProp" גם אם myProp מוחק תכונות קיימות:
function addIndexPropAssert<T extends AnyObj>(
obj: T[],
myProp: string
): T[] {
return obj.map(item => {
(item as any)[myProp] = 'myProp';
return item;
});
}
הדבר הבטוח ביותר לעשות הוא לא לשנות את האלמנטים המקוריים של obj ולהשתמש בסוגים יותר ביטויייים כדי לציין שאתה מוסיף או מחליף את התכונה [myProp] של T:
function addIndexPropDoNotModify<T, K extends PropertyKey>(obj: T[], myProp: K) {
return obj.map((item: Omit<T, K>) => expandType(Object.assign(
{ [myProp]: "myProp" } as Record<K, string>,
item
)));
}
כאן אנחנו משתמשים ב-Object.assign() כאשר היעד הוא אובייקט חדש, כך שאף אובייקט קיים לא משתנה. הסוג בסופו של דבר יהיה מערך של החיתוך של Omit<T, K> (כלומר T עם תכונת [myProp] הוסרה) עם Record<K, string> (כלומר תכונת [myProp] נוספה שוב עם ערך string).
ו-expandType() היא רק פונקציית עזר שמפתה את הקומפיילר להפוך חפיפה מכוערת של סוגים ממופים כמו Omit<{a: number, b: number}, "a"> & Record<"a", string> לסוג יותר ברור כמו {a: string, b: number}, כך:
function expandType<T>(x: T) {
return x as any as T extends infer U ? { [K in keyof U]: T[K] } : never;
}
הנה איך זה עובד:
let obj2 = { a: 123, b: 345 }; // {a: number, b: number}
const addedProps2 = addIndexPropDoNotModify([obj2], "a"); // Array<{a: string, b: number}>
addedProps2[0].a.toFixed(); // caught error at compile time, toFixed does not exist on string
אז ההבדל כאן הוא שהאלמנטים של addedProps2 נראים עכשיו כטיפוס {a: string, b: number} למרות ש-obj הוא מהטיפוס {a: number, b: number}.
בכל מקרה, אני מקווה שאחד מהם יעזור לתת לך קצת כיוון. בהצלחה!
קישור למזחלת לקוד
