Skip to content

Exhaustiveness checking with runtime error and assertNever#3599

Open
whilenot-dev wants to merge 2 commits into
microsoft:v2from
whilenot-dev:narrowing-with-assert-never
Open

Exhaustiveness checking with runtime error and assertNever#3599
whilenot-dev wants to merge 2 commits into
microsoft:v2from
whilenot-dev:narrowing-with-assert-never

Conversation

@whilenot-dev

@whilenot-dev whilenot-dev commented Jul 1, 2026

Copy link
Copy Markdown

The example function getArea in the documentation on exhaustiveness checking currently creates a conflicting behavior between compile time and run time:

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

The advantage of catching errors during compilation with help of the never-type can only occur if the missing type (Triangle) is known and has already been implemented to be a variant of type Shape. While this is a helpful check during compilation, it can lead to false assumptions on the behavior at runtime.

Where the cases "circle" and "square" would both return a value of type number back to the caller, the default case currently would just fall back to returning the input argument of an "unknown" variant of type Shape, e.g. Triangle. Such a practice could lead to problems in other parts of the code base at runtime, depending on where the Shape-objects are originating from.

In practice, it is likely that objects of type Shape are members of response objects from API calls or any other IO results, so our type implementation of type Shape = Circle | Square might just be mirroring an expectation of these response-/result-objects. We might not be in charge of their design and are merely a consuming client process. In such cases we might have an outdated implementation running that will actually fall back to the current default case at runtime, returning a "yet unknown" object of type Triangle back to its caller.

This caller has been implemented to expect a value of type number, as backed by static type checking, so any further algebraic operation on that return value (+, -, *, / etc.) would then throw a runtime error in parts of the code base that shouldn't worry about such a mistake in the first place.

Therefor, it'd be a better practice to throw an explicit runtime error in such a case. Python has assert_never for exactly these occasions.

My proposal is to change the example slightly by throwing a runtime error as fallback in the default case with the help of a function called assertNever:

// ...type definitions

function getArea(shape: Shape) {
  switch (shape.kind) {
    // ...implemented cases
    default:
      assertNever(shape);
  }
}

function assertNever(value: never): never {
  throw new Error(`Missing case for value: ${value}`);
}

@whilenot-dev

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant