Reputation: 1957
SupportedParameter
is a union of supported Parameter
enum values. When providing values for details
array I get this error.
Types of property 'parameter' are incompatible. Type 'Parameter' is not assignable to type 'SupportedParameter'.
I understand Parameter.ONE
/Parameter.THREE
aren't exactly SupportedParameter
but they are part of it. What can I do to satisfy details
type? I could do Parameter.ONE as SupportedParameter
but that's just getting rid of the error, not solving the problem.
Argument parameter
passed into isParameterEnabled
also complains about it being a Parameter
and not SupportedParameter
even though all parameter properties in details are part of SupportedParameter
.
enum Parameter {
ONE = 'ONE',
TWO = 'TWO',
THREE = 'THREE'
}
type SupportedParameter = Parameter.ONE | Parameter.THREE;
type Detail = { label: string; };
const parametersConfig: Record<SupportedParameter, { enabled: boolean }> = {
[Parameter.ONE]: {enabled: true},
[Parameter.THREE]: {enabled: false},
}
const isParameterEnabled = (parameter: SupportedParameter) => {
return parametersConfig[parameter].enabled;
}
const details: {label: string; parameter: SupportedParameter }[] = [{
label: 'One',
parameter: Parameter.ONE,
}, {
label: 'Three',
parameter: Parameter.THREE
}].filter(({ parameter }) => isParameterEnabled(parameter));
Upvotes: 1
Views: 68
Reputation: 330216
Preface: This should go without saying, but the compiler does not have human-level intelligence and cannot simply "see" the intent of the developer. It follows a set of heuristics and other fixed rules, and any benefit it would get of adding another rule or type of rule needs to outweigh the performance penalty of following such a new rule in all the cases in which it doesn't help. There's always going to be code that looks obvious to a human being that the compiler has trouble with, until we reach the singularity.
Let's pare this example down to the bare minimum:
const foo: SupportedParameter[] = [Parameter.ONE]; // okay
const bar: SupportedParameter[] = [Parameter.ONE].filter(x => x) // error
This is a limitation of TypeScript's type inference. When the type checker sees the value Parameter.ONE
, it needs to decide what type it should infer for that value. Is it just the very specific type Parameter.ONE
? Or the full enum type Parameter
? Or something wider like string
or unknown
? The heuristic rule that is used in general for an enum value is that the full enum type Parameter
is inferred unless there is some hint to do something else.
The reason why foo
above works is because TypeScript can use contextual typing to interpret the array literal [Parameter.ONE]
as conforming to SupportedParameter[]
. It sort of acts "backwards" (at runtime the array literal is created first and then assigned to the variable, but contextual typing infers the type of the array literal from the expected type of the variable) and has to reach inside [Parameter.ONE]
to Parameter.ONE
and interpret it as a narrower type than the default inferred type of Parameter
.
Contextual typing has its limits, though... it doesn't work backwards through an arbitrary number of statements or nested expressions. This would be implausible in general, since it would be prohibitively expensive to propagate inferences very far, and it would generate lots of partial/generic/candidate inferences depending on how far it decided to propagate. Consider the bar
example. In order for that to compile, the compiler would need to look at [Parameter.ONE].filter(x => x)
and say that whatever that filter()
method is, it must return a SupportedParameter[]
. But what is that filter()
method? Well, it's a method of [Parameter.ONE]
, which is some array literal. So it's some array type, but which type? We don't know yet. The compiler would need to either start making guesses ("Maybe it's Parameter[]
? No, that fails. Should I widen it? Maybe unknown[]
? No, that still fails. Narrow it to Parameter.ONE[]
? Okay, that works) or do some higher order reasoning about arrays (okay, this is an array of some generic type T[]
, and therefore the filter()
method, defined as having two overloaded call signatures could either return S[]
for some S extends T
or else it would return T[]
, and since we want that to be SupportedParameter[]
, then T
needs to be assignable to SupportedParameter
if we use the second overload, or something else if we use the first overload, so therefore ...). Either way that's a lot of extra processing that, most of the time, would be completely useless.
So what happens instead? The compiler just does the normal inference. The type of [Parameter.ONE]
is inferred as Parameter[]
, and then the filter()
method returns Parameter[]
, and then there's an error.
The fix here is to just tell the compiler to interpret Parameter.ONE
more narrowly when you first use it. The easiest way to do this is via a const
assertion, Parameter.ONE as const
. That tells the compiler that you want the most specific inference it can make, which will just be the literal type Parameter.ONE
. Then things just work:
const baz: SupportedParameter[] = [Parameter.ONE as const].filter(x => x) // okay again
Your more complex example also works if you do this:
const details: { label: string; parameter: SupportedParameter }[] = [{
label: 'One',
parameter: Parameter.ONE as const,
}, {
label: 'Three',
parameter: Parameter.THREE as const
}].filter(({ parameter }) => isParameterEnabled(parameter));
Upvotes: 2