Discriminated union types by ahejlsberg · Pull Request #9163 · microsoft/TypeScript (original) (raw)
@jesseschalken There are a surprising number of interconnected issues in the reachability, control flow, and exhaustiveness topics. I will try to explain in the following.
Our reachability analysis is based purely on the structure of your code, not on the higher level type analysis. For example, if you have code immediately following a return
or throw
statement, we know from the structure of your code that it is unreachable. But in this example
function area(s: Shape): number { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.width * s.height; case "circle": return Math.PI * s.radius * s.radius; } // Unreachable? }
we can't conclude from the structure of the code that end point is unreachable. Indeed, someone might pass you a shape with an invalid kind
and you may want to write code to guard against that:
function area(s: Shape): number { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.width * s.height; case "circle": return Math.PI * s.radius * s.radius; } fail("Invalid shape"); }
But even if fail
never returns (i.e. if it returns never
), we still can't tell from the structure of the code that the end point is unreachable. However, once you return
the value of fail
function area(s: Shape): number { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.width * s.height; case "circle": return Math.PI * s.radius * s.radius; } return fail("Invalid shape"); }
we now know from the structure of the code that the end point is unreachable. Thus, there is no implicit return of undefined
. Furthermore, because fail
returns never
and because never
is ignored in combination with other types (i.e. number | never
is just number
), everything works out.
To get exhaustiveness checks, we use a slight twist on the above and pass the guarded object as an argument to a never
returning function that expects a never
parameter:
function area(s: Shape): number { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.width * s.height; case "circle": return Math.PI * s.radius * s.radius; } return assertNever(s); // Error unless type of s is never }
Now, returning to the original example:
function area(s: Shape): number { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.width * s.height; case "circle": return Math.PI * s.radius * s.radius; } }
You'd think the above would be an error because the structure of the code does not preclude the implicit return of undefined
at the bottom of the function. The reason you don't get an error here is that when a switch
statement is the last statement of a function, and when that switch
statement has an exhaustive set of cases for the type of the switch expression, we suppress the undefined
that would have come from the implicit return at the end of the function. This happens in the type checking phase as part of return type analysis and is one of the parts of this pull request.