Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript - narrowing type from generic union type

//Type declaration:

interface TickListFilter {
   type: "tickList";
   value: string;
}

interface ColorFilter {
   type: "color"
   value: ColorValueType
}

type Filter = TickListFilter | ColorFilter;



...
onValueChange = (filter: Filter, newValue: Filter["value"]) => {
        if (filter.type === "tickList") {
            // variable 'filter' has TickListFilter type (OK)
            // variable 'filter.value' has string type (OK)
            // variable newValue has ColorValueType | string. I know why, let's fix it!
        }
...

onValueChange = <T extends Filter>(filter: T, newValue: T["value"]) => {
        if (filter.type === "tickList") {
            // variable 'filter' has Filter type (wrong... i need tick list)
            // variable 'filter.value' has string | ColorValueType type (wrong, it should be string)
            // variable newValue has ColorValueType | string.
        }

How I can fix that? I know why it happens, because TS cant discriminate union by generic type. But is there any workaround to work as I described?

I want to use similar structure as switch-case (not if-else only)

like image 633
Kakaku Avatar asked Jun 22 '26 23:06

Kakaku


1 Answers

Ah, welcome to microsoft/TypeScript#13995 and microsoft/TypeScript#24085. Currently the compiler does not use control flow analysis to narrow generic type parameters or values of types dependent on generic type parameters. Technically it would be wrong to narrow the type parameters themselves, since someone could come along and call the generic version this way:

const filter: Filter = Math.random() < 0.5 ?
  { type: "tickList", value: "str" } : { type: "color", value: colorValue };
const newValue: string | ColorValueType = Math.random() < 0.5 ? "str" : colorValue;
onValueChange(filter, newValue); // no compiler error
// const onValueChange: <Filter>(filter: Filter, newValue: string | ColorValueType) => void

The compiler will infer Filter for T, which is "correct" according to the generic signature, but it leads to the same problem as your non-generic version. Inside the function it may be that newValue doesn't match filter. 😢


There have been a number of suggestions and discussions inside the above GitHub issue on ways to deal with this problem. If something like microsoft/TypeScript#27808, you could say something like T extends-exactly-one-member--of Filter meaning that T could be TickListFilter or ColorFilter but it could not be Filter. But right now there's no way to say this.


For now the easiest way to proceed (assuming you don't want to actually check inside the implementation of your function that filter and newValue match each other) is going to involve type assertions or something like them. You need to loosen type checking enough to allow the code, and just expect/hope that nobody will call your function the way I did above.

Here's one way using type assertions:

const onValueChange = <T extends Filter>(filter: T, newValue: T["value"]) => {
  const f: Filter = filter; // widen to concrete type
  if (filter.type === "tickList") {
    const v = newValue as TickListFilter["value"]; // technically unsafe narrowing
    f.value = v; // okay
  } else {
    const v = newValue as ColorFilter["value"]; // technically unsafe narrowing
    f.value = v; // okay
  }
}

Playground link to code

like image 186
jcalz Avatar answered Jun 24 '26 14:06

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!