I have the follow components and function:
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
My intention is to be able to mix different types for the keys, so some could be a string, some could be a number, and some could be a boolean. My intention is to infer the type from the labels and apply that to other props, such as defaultKey
or onSubmit
that benefit from literal types. Otherwise, type guarding is required to ensure only the keys are acceptable values to other functions, which places responsibility on the engineer for every use case. I would like to avoid this.
I am perfectly able to call the function when all the keys are of the same type.
testFunc({
labels: [{
label: 'whatever',
key: 'a',
}, {
label: 'whatever',
key: 'b',
}, {
label: 'whatever',
key: 'c',
}],
defaultValue: 'a',
onChange: (state) => {}
}) // Correctly assigns type IProps<'a' | 'b' | 'c'>
However, whenever I try mixing types, typescript thinks that T
is a constant of the first index, and throws an error for all the other values. Example:
testFunc({
labels: [{
label: 'whatever',
key: 'a',
}, {
label: 'whatever',
key: 'b',
}, {
label: 'whatever',
key: 2,
}],
defaultValue: 'c',
onChange: (state) => {}
}) // Errors because it thinks the type is 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">'
There is a way to mix them by using primitives. For example, the following works:
enum TestEnum {
ONE = 'one',
TWO = 'two',
THREE = 3,
}
enum TestEnum2 {
FOUR = 'four',
FIVE = 'five',
SIX = 6,
}
testFunc({
labels: [{
label: 'whatever',
key: TestEnum.ONE,
}, {
label: 'whatever',
key: TestEnum.TWO,
}, {
label: 'whatever',
key: TestEnum.THREE,
}],
defaultValue: TestEnum.TWO,
onChange: (state) => {}
}) // Correctly works as IProps<TestEnum>
However, all keys must be of the type TestEnum
. Mixing with primitive literals or other enum values also results in 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 because it infers as IProps<TestEnum.ONE>
// Mixing enum and literal
testFunc({
labels: [{
label: 'whatever',
key: TestEnum.ONE,
}, {
label: 'whatever',
key: 'two',
}],
defaultValue: 'two',
onChange: (state) => {}
}) // Errors because it infers as IProps<TestEnum.ONE>
What explanation is there for typescript to be confused? Or is there a logical explanation?
I understand the use case of mixing string and number literals is very well, but it is likely an engineer would try mixing different enums, or enums with literals and be confused with this ambiguous error.