I am making a combobox component which takes two parameters:
options
: a series of objects to select fromdisplaytextKeyname
: a string which represents the name of a key to look at within eachoption
which contains a string to display as the name
Here is in a simplified form:
type ComboboxProps<T> = {
options: T[];
displaytextKeyname: string;
}
const Combobox = ({
options,
displaytextKeyname
}:ComboboxProps<unknown>) => {
options.forEach(o => console.log(o[displayTextKeyname])
}
I am looking to make an object that would be suitable as type T
above. The idea is this is designed in such a generic way that said field could be called "description"
, "longname"
, "name"
or whatever. All that is required is that it is a string.
Currently I have attempted it in the following ways:
Approach 1 - require all string values
type ComboboxOption = {
[key: string]: string;
};
const foo: ComboboxOption = {
a: "123",
b: 456, // Type 'number' is not assignable to type string
};
Approach 2 - allow all values
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,
};
Approach 3 - extend interface
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;
}
Is this beyond typescript's type system or am I just thinking about this entirely wrong?
CodePudding user response:
Yes, such a constraint is indeed too much for TypeScript types. A stand-alone type will not be able to express this behaviour.
But it is possible to achieve things like this with a generic function. This would be my approach:
function Combobox<
Key extends string
>(key: Key, options: (Record<string, any> & Record<Key, string>)[]) {}
This will validate the passed array based on the constraint you specified.
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>'
}])
CodePudding user response:
While @TobiasS.'s answer is perfectly valid, it is possible to build a solution that is much closer to your initial description:
- using object destructuring, for a more React component-like signature (can be easily adapted to @TobiasS.'s answer as well)
- using a standalone type, without necessarily applying it to a function (but only when used with a function can the generic be automatically inferred)
// 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 }>
}
Example using the type without a function:
// 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; }'.
}]
}
Example using the type in a function:
// 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; }'.
}]
})