LazioTibijczyk
LazioTibijczyk

Reputation: 1957

Union containing Enum values

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));

TypeScript Playground

Upvotes: 1

Views: 68

Answers (1)

jcalz
jcalz

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));

Playground link to code

Upvotes: 2

Related Questions