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?
Simple answer: TypeScript is just not that powerful. Your logic is correct, you can determine the type of
items
based on the type ofsource
, 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
anditems
are differently typed attributes and are treated separately. Imagine it like so: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
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.