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.