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
itemsbased 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,
sourceanditemsare 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
Tis 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.