Skip to content

Latest commit

 

History

History
264 lines (221 loc) · 12.8 KB

exhaustiveness.md

File metadata and controls

264 lines (221 loc) · 12.8 KB

Item 59: Use Never Types to Perform Exhaustiveness Checking

Things to Remember

  • Use an assignment to the never type to ensure that all possible values of a type are handled (an "exhaustiveness check").
  • Add a return type annotation to functions that return from multiple branches. You may still want an explicit exhaustiveness check, however.
  • Consider using template literal types to ensure that every combination of two or more types is handled.

Code Samples

type Coord = [x: number, y: number];
interface Box {
  type: 'box';
  topLeft: Coord;
  size: Coord;
}
interface Circle {
  type: 'circle';
  center: Coord;
  radius: number;
}
type Shape = Box | Circle;

💻 playground


function drawShape(shape: Shape, context: CanvasRenderingContext2D) {
  switch (shape.type) {
    case 'box':
      context.rect(...shape.topLeft, ...shape.size);
      break;
    case 'circle':
      context.arc(...shape.center, shape.radius, 0, 2 * Math.PI);
      break;
  }
}

💻 playground


interface Line {
  type: 'line';
  start: Coord;
  end: Coord;
}
type Shape = Box | Circle | Line;

💻 playground


function processShape(shape: Shape) {
  switch (shape.type) {
    case 'box': break;
    case 'circle': break;
    case 'line': break;
    default:
      shape
      // ^? (parameter) shape: never
  }
}

💻 playground


function processShape(shape: Shape) {
  switch (shape.type) {
    case 'box': break;
    case 'circle': break;
    // (forgot 'line')
    default:
      shape
      // ^? (parameter) shape: Line
  }
}

💻 playground


function assertUnreachable(value: never): never {
  throw new Error(`Missed a case! ${value}`);
}

function drawShape(shape: Shape, context: CanvasRenderingContext2D) {
  switch (shape.type) {
    case 'box':
      context.rect(...shape.topLeft, ...shape.size);
      break;
    case 'circle':
      context.arc(...shape.center, shape.radius, 0, 2 * Math.PI);
      break;
    default:
      assertUnreachable(shape);
      //                ~~~~~
      // ... type 'Line' is not assignable to parameter of type 'never'.
  }
}

💻 playground


function drawShape(shape: Shape, context: CanvasRenderingContext2D) {
  switch (shape.type) {
    case 'box':
      context.rect(...shape.topLeft, ...shape.size);
      break;
    case 'circle':
      context.arc(...shape.center, shape.radius, 0, 2 * Math.PI);
      break;
    case 'line':
      context.moveTo(...shape.start);
      context.lineTo(...shape.end);
      break;
    default:
      assertUnreachable(shape); // ok
  }
}

💻 playground


function getArea(shape: Shape): number {
  //                            ~~~~~~ Function lacks ending return statement and
  //                                   return type does not include 'undefined'.
  switch (shape.type) {
    case 'box':
      const [width, height] = shape.size;
      return width * height;
    case 'circle':
      return Math.PI * shape.radius ** 2;
  }
}

💻 playground


function getArea(shape: Shape): number {
  switch (shape.type) {
    case 'box':
      const [width, height] = shape.size;
      return width * height;
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'line':
      return 0;
    default:
      return assertUnreachable(shape);  // ok
  }
}

💻 playground


function processShape(shape: Shape) {
  switch (shape.type) {
    case 'box': break;
    case 'circle': break;
    default:
      const exhaustiveCheck: never = shape;
      //    ~~~~~~~~~~~~~~~ Type 'Line' is not assignable to type 'never'.
      throw new Error(`Missed a case: ${exhaustiveCheck}`);
  }
}

💻 playground


function processShape(shape: Shape) {
  switch (shape.type) {
    case 'box': break;
    case 'circle': break;
    default:
      shape satisfies never
      //    ~~~~~~~~~ Type 'Line' does not satisfy the expected type 'never'.
      throw new Error(`Missed a case: ${shape}`);
  }
}

💻 playground


type Play = 'rock' | 'paper' | 'scissors';

function shoot(a: Play, b: Play) {
  if (a === b) {
    console.log('draw');
  } else if (
    (a === 'rock' && b === 'scissors') ||
    (a === 'paper' && b === 'rock')
  ) {
    console.log('A wins');
  } else {
    console.log('B wins');
  }
}

💻 playground


function shoot(a: Play, b: Play) {
  const pair = `${a},${b}` as `${Play},${Play}`;  // or: as const
  //    ^? const pair: "rock,rock" | "rock,paper" | "rock,scissors" |
  //                   "paper,rock" | "paper,paper" | "paper,scissors" |
  //                   "scissors,rock" | "scissors,paper" | "scissors,scissors"
  switch (pair) {
    case 'rock,rock':
    case 'paper,paper':
    case 'scissors,scissors':
      console.log('draw');
      break;
    case 'rock,scissors':
    case 'paper,rock':
      console.log('A wins');
      break;
    case 'rock,paper':
    case 'paper,scissors':
    case 'scissors,rock':
      console.log('B wins');
      break;
    default:
      assertUnreachable(pair);
      //                ~~~~ Argument of type "scissors,paper" is not
      //                     assignable to parameter of type 'never'.
  }
}

💻 playground