Skip to content

Item 59: Use Never Types to Perform Exhaustiveness Checking

要点

  • 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.
  • 使用赋值给 never 类型来确保所有可能的类型值都被处理(“穷尽性检查”)。
  • 为返回多个分支的函数添加返回类型注解。尽管如此,你仍然可能需要显式的穷尽性检查。
  • 考虑使用模板字面量类型来确保每种两种或更多类型的组合都被处理。

正文

ts
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


ts
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


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

💻 playground


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

💻 playground


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

💻 playground


ts
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


ts
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


ts
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


ts
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


ts
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


ts
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


ts
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


ts
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

Released under the MIT License.