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


אני יוצר רכיב combobox שלוקח שני פרמטרים:

  1. options: סדרת אובייקטים לבחירה
  2. 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; }'.
  }]
})

קישור למגרש משחקים