TypeScript - איך ליצור טיפוס עם מפתח אחד או יותר בעלי ערך מחרוזת תוך מתן אפשרות לשדות אחרים להיות מכל סוג?
אני יוצר רכיב combobox שלוקח שני פרמטרים:
options: סדרת אובייקטים לבחירהdisplaytextKeyname: מחרוזת שמייצגת את שם המפתח שבו יש להסתכל בתוך כלoptionהמכילה מחרוזת להצגה כשם
הנה דוגמה מפושטת של זה בשימוש:
type ComboboxProps<T> = {
options: T[];
displaytextKeyname: string;
}
const Combobox = ({
options,
displaytextKeyname
}:ComboboxProps<unknown>) => {
options.forEach(o => console.log(o[displayTextKeyname])
}
אני מחפש ליצור אובייקט שיתאים כסוג T למעלה. הרעיון הוא שזה מעוצב בצורה כל כך גנרית שהשדה הזה יכול להיקרא "תיאור", "שם ארוך", "שם" או משהו כזה. כל מה שנדרש הוא שזה מחרוזת.
כעת ניסיתי זאת בדרכים הבאות:
גישה 1 - לדרוש את כל ערכי המחרוזת
type ComboboxOption = {
[key: string]: string;
};
const foo: ComboboxOption = {
a: "123",
b: 456, // Type 'number' is not assignable to type string
};
גישה 2 - לאפשר את כל הערכים
type ComboboxOption = {
[key: string]: unknown;
};
const foo: ComboboxOption = {
a: "123",
b: 123,
};
// This allows objects which do not have at least one key with a string value
const bar: ComboboxOption = {
b: 123,
};
גישה 3 - להרחיב ממשק
interface IComboboxOption {
[key: string]: string;
}
//Interface 'IComboboxOptionExt' incorrectly extends interface 'IComboboxOption'.
//'string' index signatures are incompatible.
//Type 'unknown' is not assignable to type 'string'
interface IComboboxOptionExt extends IComboboxOption {
[key: string | number | symbol]: unknown;
}
האם זה מעבר למערכת סוגי הטייפסקריפט או שאני פשוט חושב על זה לגמרי לא נכון?
תשובה #1
כן, הגבלה כזו אכן היא יותר מדי עבור טיפוסי TypeScript. טיפוס עצמאי לא יהיה מסוגל להביע את ההתנהגות הזאת.
אבל אפשר להשיג דברים כאלה עם פונקציה כללית. זו תהיה הגישה שלי:
function Combobox<
Key extends string
>(key: Key, options: (Record<string, any> & Record<Key, string>)[]) {}
זה יאמת את המערך שהועבר על פי המגבלה שציינת.
Combobox("description", [{
"description": "abc",
a: 123
}])
Combobox("longname", [{
"longname": "abc",
a: 123
},{
"longname": "efg",
b: 456
}, {
c: 123 // Error: '{ c: number; }' is not assignable to type 'Record<string, any> & Record<"longname", string>'
}])
מִגרָשׂ מְשָׂחָק
תשובה #2
בעוד שהתשובה של @TobiasS. תקפה בהחלט, ניתן לבנות פתרון הקרוב הרבה יותר לתיאור הראשוני שלך:
- שימוש בפירוק אובייקטים, עבור חתימה דומה יותר לרכיב React (ניתן בקלות להתאים גם לתשובה של @TobiasS.)
- שימוש בסוג עצמאי, ללא צורך בהחלתו על פונקציה (אך רק כאשר הוא משמש עם פונקציה ניתן להסיק את הגנרי באופן אוטומטי)
// Use a generic for the key instead of the array type
type ComboboxProps<Key extends string> = {
displaytextKeyname: Key,
// Constrain the array type using the key, like in @TobiasS.'s answer
options: Array<Record<string, any> & { [k in Key]: string }>
}
דוגמה לשימוש בסוג ללא פונקציה:
// Using the type standalone,
// but we have to explicitly specify the generic type
const ComboboxObject: ComboboxProps<"hello"> = {
displaytextKeyname: "hello",
options: [{
hello: "world",
foo: 42
}, {
// @ts-expect-error
hello: false // Type 'boolean' is not assignable to type 'string'.
},
// @ts-expect-error
{
foo: "bar" // Type '{ foo: string; }' is not assignable to type 'Record<string, any> & { hello: string; }'. Property 'hello' is missing in type '{ foo: string; }' but required in type '{ hello: string; }'.
}]
}
דוגמה לשימוש בסוג בתוך פונקציה:
// Using the type for a function argument,
// the generic can be automatically inferred.
const Combobox = <Key extends string>({
options,
displaytextKeyname
}: ComboboxProps<Key>) => {
options.forEach(o => console.log(o[displaytextKeyname]))
}
Combobox({
displaytextKeyname: "description",
options: [{
"description": "abc",
any: true
}]
})
Combobox({
displaytextKeyname: "longname",
options: [{
"longname": "abc",
a: 123
}, {
// @ts-expect-error
"longname": 4, // Type 'number' is not assignable to type 'string'.
b: 456
},
// @ts-expect-error
{
c: 123 // Type '{ c: number; }' is not assignable to type 'Record<string, any> & { longname: string; }'. Property 'longname' is missing in type '{ c: number; }' but required in type '{ longname: string; }'.
}]
})
קישור למגרש משחקים
