Typescript - Conditional object property does not produce outer discriminate union

61 views Asked by At

In this code, I am expecting to be able to assert that __typename is "Batch":

export type State<T extends "Batch" | "Shipment" = "Batch" | "Shipment"> = {
    source: T
    items: T extends "Batch" ? { __typename: "Batch" }[] : { __typename: "Shipment" }[]
}

const state: State = {
    source: "Batch",
    items: []
}

if (state.source === "Batch") {
    state.items.map((i) => {
        const typename: "Batch" = i.__typename // error
    })
}

However narrowing on state.source does nothing to narrow the type of state. It remains as State<"Batch" | "Shipment"> instead of State<"Batch">. Playground

I found that refactoring the type to a top-level union fixes the problem:

export type State<T extends "Batch" | "Shipment" = "Batch" | "Shipment"> = T extends "Batch" ? {
    source: T
    items: { __typename: "Batch" }[]
} : {
    source: T
    items: { __typename: "Shipment" } []
}

but why is this?

1

There are 1 answers

1
KnightsWhoSayNi On

Simple answer: TypeScript is just not that powerful. Your logic is correct, you can determine the type of items based on the type of source, but TypeScript just does not go the extra mile to do that. Perhaps they will make it smarter in some future version, we'll see, but it's not really needed to be honest.

From the point of view of TypeScript, source and items are differently typed attributes and are treated separately. Imagine it like so:

export type State<T extends "Batch" | "Shipment" = "Batch" | "Shipment"> = {
    source: T
    items: U
}

That's why it does not infer one based on the other.

Your second type works, because the early condition makes it clear that for your whole type the value of T is one specific value - either "Batch" or "Shipment".

EXTRA

In your situation I would recommend typing it like this

export type State<T extends "Batch" | "Shipment" = "Batch" | "Shipment"> = T extends (infer V) ? {
  source: V
  items: { __typename: V }[]
} : never;

This way you'll be able to add additional values to your union type (e.g. "Batch" | "Shipment" | "Product") and it will still work as intended.