I have a relatively generic component. Its definition looks something like the following:
export type CheckboxItem = {
label: string,
code: string,
};
export type CheckboxesProps = {
items: CheckboxItem[],
handleStateChange: (selected: (CheckboxItem['code'])[]) => void,
};
export interface CheckboxState<T> {
[key: CheckboxItem['code']]: boolean,
}
export default function Checkboxes({items, handleStateChange}: CheckboxesProps) {
const [state, setState] = useState<CheckboxState>(items.reduce((stateObj: CheckboxState, item: CheckboxItem) => {
stateObj[item.code] = false;
return stateObj;
}, {}));
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setState({
...state,
[event.target.name]: event.target.checked,
});
handleStateChange(Object.keys(state).filter(key => state[key]));
};
...
}
This works until I try invoking Checkboxes with items that are Object Constants (not sure correct typescript terminology):
// within another component
export const PrimaryNeeds = {
foodAllergies: 'foodAllergies',
foodRestrictions: 'foodRestrictions',
medicineAllergies: 'medicineAllergies',
medicalConditions: 'medicalConditions',
phobias: 'phobias',
other: 'other',
} as const;
// type checkboxItems = {
// label: PrimaryNeeds[keyof PrimaryNeeds]
// code: keyof PrimaryNeeds,
// }
// The above type isn't actually in my code, I simply do the following instead:
export type PrimaryNeedsCheckboxesProps = {
langCode: keyof typeof LanguageCodes,
handlePrimaryNeedsChanged: (selected: (keyof typeof PrimaryNeeds)[]) => void,
}
export default function PrimaryNeedsCheckboxes({langCode, handlePrimaryNeedsChanged}: PrimaryNeedsCheckboxesProps) {
const checkboxItems = (Object.keys(PrimaryNeeds) as (keyof PrimaryNeeds)[]).map((needKey: keyof PrimaryNeeds) => {
return {
label: primaryNeeds[needKey],
code: needKey,
};
});
// ...
return (
<Checkboxes items={checkboxItems} handleStateChange={handlePrimaryNeedsChanged} />
)
Now I get an error about type mismatch between the type string not being assignable to the I guess "union type" of the PrimaryNeeds keys:
Type '(selected: ("foodAllergies" | "foodRestrictions" | "medicineAllergies" | "medicalConditions" | "phobias" | "other")[]) => void' is not assignable to type '(selected: string[]) => void'.
Types of parameters 'selected' and 'selected' are incompatible.
Type 'string[]' is not assignable to type '("foodAllergies" | "foodRestrictions" | "medicineAllergies" | "medicalConditions" | "phobias" | "other")[]'.
Type 'string' is not assignable to type '"foodAllergies" | "foodRestrictions" | "medicineAllergies" | "medicalConditions" | "phobias" | "other"'.
I tried to set a generic type so that the Checkbox component CheckboxItem object property types could depend on a passed-in type, but then I ran into typescript not allowing generic types to be used as index:
export type CheckboxItem<T> = {
label: keyof T,
code: T[keyof T],
};
export type CheckboxesProps<T>= {
items: CheckboxItem<T>[],
handleStateChange: (selected: (CheckboxItem<T>['code'])[]) => void,
};
// error points to "key" below
export interface CheckboxState<T> {
[key: CheckboxItem<T>['code']]: boolean,
}
An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
I read the mapped object documentation but didn’t see how that could help, though I don’t exactly understand the mechanism of how they’re supposed to help either. Nor do I understand the issue around using a generic type as an index signature parameter – it seems to be what I want, though I guess there’s a good reason.
I did try using the bit recommended in Key Remapping:
export interface CheckboxState<T> {
[K in T]?: boolean,
}
But Typescript didn’t like that either:
A mapped type may not declare properties or methods.
How can I create a generic type for these props? Or, is that what I should even be doing here?
2
Answers
type
andinterface
in TypeScript are mostly equivalent, but mapped types are an exception – they have to be types.So
throws an error, but
works.
(It also looks like your example code may be confusing codes and labels – your commented-out
checkboxItems
uses code as the keys and label as the values (I assume this is what you need), but yourCheckboxItems
generic type uses label as the keys and code as the value.)I suppose you could start with a
useCheckboxes
hook –Each instance of
useCheckboxes
returns a unique type –We get helpful type auto-completions when using the
set
andget
methods –Now you can write your
Checkboxes
component.UseCheckboxes
gives us everything we need to iterate and display the checkboxes, and create anonChange
handler for each checkbox –Here’s a complete demo you can run in your browser –