ページ一覧を開く

Value Objectで空欄をどう表す?

値が存在しないなら生成しない

画面でまだ入力されていない空欄と、業務上有効な値は分けて扱います。入力途中の空欄は画面側で持ち、Value Objectには変換しません。

Value Objectは、業務上意味のある正しい値だけを表します。そのため、中身が空文字やnullのValue Objectは基本的に作りません。

値が単に設定されていない場合は、Entityの該当プロパティをnullにできます。「制限なし」のように値がないこと自体が業務上の状態なら、その状態をValue Objectで明示します。

入力途中の空欄

例えば、募集を作る画面の「開催場所」は、入力前には空欄です。この状態は画面へ入力した値として持ちます。

// 画面へ入力した値を持つ型
type RecruitmentFormInput = {
  location: string; // 入力前は空文字
};

入力途中では、まだ有効な開催場所とは限らないため、Value Objectを作りません。

必須項目が空欄の場合

開催場所が必須なら、登録するときに空欄をエラーにします。有効な値を確認できた場合だけValue Objectへ変換します。

const location = input.location.trim();

// 必須項目が空欄なら、Value Objectを作らない
if (location === "") {
  throw new Error("開催場所を入力してください");
}

// 有効な値だけをValue Objectへ変換する
const recruitmentLocation =
  new RecruitmentLocationValueObject(location);

任意項目に値がない場合

例えば、フットサルNOWの募集には、任意で「年齢制限」を設定できるとします。

設定されていないことを表す

年齢制限を単なる任意項目として扱うなら、年齢制限がある場合だけValue Objectを作ります。設定されていない場合は、Entityが持つ年齢制限をnullにします。

type RecruitmentProps = {
  // 年齢制限がない場合はnull
  ageRestriction: AgeRangeValueObject | null;
};

// 18歳から39歳まで参加できる募集
const restrictedRecruitment: RecruitmentProps = {
  ageRestriction: new AgeRangeValueObject({
    minimumAge: 18,
    maximumAge: 39,
  }),
};

// 年齢制限がない募集
const unrestrictedRecruitment: RecruitmentProps = {
  ageRestriction: null,
};

この例では、Value Objectがあれば「年齢制限あり」、nullなら「年齢制限なし」です。

「年齢制限なし」を状態として表す

「年齢制限なし」も業務上の状態として明確に扱いたい場合は、「制限なし」と「範囲指定」を表せるValue Objectにします。

// 「制限なし」と「範囲指定」を表す値
type AgeRestrictionValue =
  | { type: "none" }
  | {
      type: "range";
      minimumAge: number;
      maximumAge: number;
    };

class AgeRestrictionValueObject {
  constructor(readonly value: AgeRestrictionValue) {}
}

const noRestriction = new AgeRestrictionValueObject({
  type: "none",
});

const restricted = new AgeRestrictionValueObject({
  type: "range",
  minimumAge: 18,
  maximumAge: 39,
});

この場合、Value Objectは常に存在し、その中に「制限なし」または「範囲指定」という有効な状態があります。最小年齢と最大年齢へ直接nullを入れる必要はありません。

DBで数字を管理する場合

DBでは、最小年齢と最大年齢を数値のカラムとして持ち、年齢制限がない場合だけNULLで保存できます。

age_restriction_type | minimum_age | maximum_age
none                 | NULL        | NULL
range                | 18          | 39

Repositoryは、DBのNULLを、ドメインモデルの「年齢制限なし」へ変換します。DBでNULLを使っていても、Value Objectの年齢へnullを入れる必要はありません。

type AgeRestrictionRow =
  | {
      type: "none";
      minimumAge: null;
      maximumAge: null;
    }
  | {
      type: "range";
      minimumAge: number;
      maximumAge: number;
    };

// RepositoryでDBのデータをValue Objectへ変換する
function toAgeRestriction(row: AgeRestrictionRow) {
  if (row.type === "none") {
    return new AgeRestrictionValueObject({ type: "none" });
  }

  return new AgeRestrictionValueObject({
    type: "range",
    minimumAge: row.minimumAge,
    maximumAge: row.maximumAge,
  });
}

0999などの特別な数字で「制限なし」を表すと、本当の年齢と区別できなくなるため避けます。