Below are the components and function I am working with:
interface ILabel<T> {
readonly label: string;
readonly key: T
}
interface IProps<T> {
readonly labels: Array<ILabel<T>>;
readonly defaultValue: T;
readonly onChange: (state: ILabel<T>) => void;
}
const testFunc = <T extends string | number>(labels: IProps<T>) => labels
I want to mix different types for the keys, allowing some to be strings, some numbers, and some booleans. By inferring the type from the labels and applying it to other props like defaultKey
or onSubmit
, I hope to avoid the need for manual type guarding in every instance.
Calling the function works fine when all keys are of the same type:
testFunc({
labels: [{
label: 'whatever',
key: 'a',
}, {
label: 'whatever',
key: 'b',
}, {
label: 'whatever',
key: 'c',
}],
defaultValue: 'a',
onChange: (state) => {}
}) // Assigns correct type IProps<'a' | 'b' | 'c'>
However, when attempting to mix types, TypeScript incorrectly assumes that T
is a constant based on the first index only, leading to errors for subsequent values:
testFunc({
labels: [{
label: 'whatever',
key: 'a',
}, {
label: 'whatever',
key: 'b',
}, {
label: 'whatever',
// Error occurs here with mixed types
key: 2,
}],
defaultValue: 'c',
onChange: (state) => {}
})
// Errors due to incorrect inference as IProps<'a'>
Type '"b"' is not assignable to type '"a"'.ts(2322)
TabBar.types.ts(79, 12): The expected type comes from property 'key' which is declared here on type 'ILabel<"a">'
Although mixing with primitive literals or enums can work, combining different enum types or enums with literals often produces the same error:
// Mixing different enums
testFunc({
labels: [{
label: 'whatever',
key: TestEnum.ONE,
}, {
label: 'whatever',
key: TestEnum.TWO,
}, {
label: 'whatever',
key: TestEnum2.FOUR,
}],
defaultValue: TestEnum.TWO,
onChange: (state) => {}
}) // Errors with inference as IProps<TestEnum.ONE>
// Mixing enum and literal
testFunc({
labels: [{
label: 'whatever',
key: TestEnum.ONE,
}, {
label: 'whatever',
key: 'two',
}],
defaultValue: 'two',
onChange: (state) => {}
}) // Errors with inference as IProps<TestEnum.ONE>
Why does TypeScript struggle with these type ambiguities? Is there a logical explanation?
While mixing string and number literals is clear, attempting to combine different enums causes confusion and misleading errors for engineers.