LoginSignup
1
0

TypeScript 「never」を使った網羅チェック

Posted at

UNIONを使った全網羅条件分岐処理の場合、条件が追加になっても対応漏れにならないように対策をしましょう。っていう話。

ユーザーランク「A, B, C」がありました。

// ユーザーランク
type UserRankOptions = 'A' | 'B' | 'C';

ユーザーランクに以下の仕様変更が発生が発生しました。

ランクDを追加
ランクDのボーナスポイントは30とする

type UserRankOptions = 'A' | 'B' | 'C' | 'D'; // ランクD追加

:thumbsdown::thumbsdown: なにも対策していない

addBonusPointメソッドは対策をしていなので、改修が漏れた場合ランクDのボーナスポイント30が付加されず不具合の要因になる。

// ユーザーランク
type UserRankOptions = 'A' | 'B' | 'C' | 'D';

// ユーザークラス
class User {
  private _point: number;
  private _rank: UserRankOptions;
  public constructor(rank: UserRankOptions) {
    this._point = 0;
    this._rank = rank;
  }
  public get point() { return this._point }
  public set point(point: number) { this._point = point}
  public get rank() { return this._rank }
  
}

// ボーナスポイントを付加
function addBonusPoint(user: User): void {
  switch (user.rank) {
    case 'A': 
      user.point += 100;
      break;
    case 'B': 
      user.point +=  70;
      break;
    case 'C': 
      user.point +=  50;
      break;
    // ランクDの処理が漏れている!!!
  }
}

const user = new User('D');
addBonusPoint(user); // ボーナスポイント30が付加されない。。。
consolo.log(user.point); // 0

:thumbsdown: 例外エラーによる対策

予期しない値を受け取ると例外エラーを発生させ、改修漏れを防ぐ対策となります。
「D」を受け取ると例外エラーが発生するので、改修漏れでも運良ければ検証中(実行中)に発見出来きます。
改修が漏れているということはUTテストケース改修も漏れるのでUTで気付くのは難しいかも。。。。

function addBonusPoint(user: User): void {
  switch (user.rank) {
    case 'A': 
      user.point += 100;
      break;
    case 'B': 
      user.point +=  70;
      break;
    case 'C': 
      user.point +=  50;
      break;
    // 例外エラーにして、改修漏れ対策をする  
    default:
      throw new Error(`${user.rank}未対応ランク値`);  }
}

:thumbsup: neverを使った例外エラーによる対策

これも予期しない値を受け取ると例外エラーを発生させ、改修漏れを防ぐ対策となります。
neverを使っていな場合との違いは、neverを使うと「実装エラー」となることです。 
実装エラーになるので、改修実装時に改修箇所を特定できます。
image.png

// 全網羅例外エラークラス
class ExhaustiveError extends Error {
  constructor(value: never, message = `未対応値: ${value}`) {
    super(message);
  }
}

function addBonusPoint(user: User): void {
  switch (user.rank) {
    case 'A': 
      user.point += 100;
      break;
    case 'B': 
      user.point +=  70;
      break;
    case 'C': 
      user.point +=  50;
      break;
    default:
       // Dの分岐がないとエラーになる
      // Argument of type 'string' is not assignable to parameter of type 'never'.ts(2345)  
      throw new ExhaustiveError(user.rank);  }
}

おまけ

戻り値がある場合、到達不能(デッド)分岐は実装しないほうがよい?

:thumbsdown::thumbsdown: 到達不能分岐を実装する

「とりえずdefaultの場合は0でも返しておくかー」的な発想で、到達不能(デッド)分岐を実装してしまう。

type UserRankOptions = 'A' | 'B' | 'C';

function getBonusPoint(userRank: UserRankOptions): number {
  switch (userRank) {
    case 'A': 
      return 100;
    case 'B': 
      return 70;
    case 'C': 
      return 50;
    default: // 到達不能(デッド)分岐
      return 0; //0って意味のない値 
  }
}

「D」の分岐が無いためdefault分岐が「到達不能」から「到達可能」になります。
改修しないとランクDの場合「0」が戻ってくるので不具合の要因となります。

「UserRankOptions」を使用している全プログラムを調査すればよいが、大規模システムだとこの作業が結構大変だったりします。

:thumbsup: 到達不能分岐を実装しない

実装エラーになるので、改修実装時に改修箇所を特定できます。
image.png

:warning: これはTypeScriptの設定「strictNullChecks: true」になっていないとエラーにならないので注意です。

type UserRankOptions = 'A' | 'B' | 'C' | 'D';

// エラー:Function lacks ending return statement and return type does not include 'undefined'.ts(2366)
function getBonusPoint(userRank: UserRankOptions): number {
  switch (userRank) {
    case 'A': 
      return 100;
    case 'B': 
      return 70;
    case 'C': 
      return 50;
  }
}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0