TypeScriptでValue Objectを試す

# 概要

表題の通り、TypeScriptでValue Objectの実装を試したのでメモします。

# Value Objectとは？
本記事でいうValue Objectは、「DDD」に登場するValue Object（値オブジェクト）のことです。
特にここではValue Objectの説明には触れません。

参考
https://little-hands.hatenablog.com/entry/2018/12/09/entity-value-object


# 実装

## GOAL
下記の２種類のValue Objectを作成してみます。
- Email: Eメールアドレス (正しいメールアドレスかどうかバリデーション)
- Password: パスワード (８文字以上かどうかバリデーション)

```typescript
const email = new Email("xxxx@example.com") // 成功
```

```typescript
const email = Email.instantiate("xxxx@example.com") // 成功
```

```typescript
const email = new Email("xxxxxxx") // エラー
```

```typescript
const email = Email.instantiate("xxxxxxxxxx") // null
```

## Value Objectの抽象クラス
```typescript
abstract class ValueObject<T extends string, K> {
  readonly valueObjectType: T | undefined
  constructor(
    readonly value: K
  ) {
    if (!this.isValid()) {
      throw new Error()
    }
  }

  equals(v: ValueObject<T, K>): boolean {
    return this.value == v.value
  }

  protected abstract isValid(): boolean
}
```

Value Objectのabstract classを用意しました。
TypeScriptはStructural Typingな言語なため、クラスの構造が同じの場合は、同じクラスとしてみなされてしまいます。

```typescript
class Email {
    constructor(readonly value: string) {}
}

class Password {
    constructor(readonly value: string) {}
}
```
例えば上記の実装をすると、
```typescript
const email: Email = new Password("maaaa") 👉 利用できてしまう
```
こんな実装をしても、エラーで警告されません。
そこでValue Objectの抽象クラスに、valueObjectTypeというプロパティを必ず１つ持たせ、区別します。
また、等価判定のためのequalsメソッドを生やし、abstract methodとしてisValidメソッドの実装を明示しています。
isValidメソッドをコンストラクタ内部で呼び、isValidに引っかかった場合はerrorをthrowしています。
これにより、Value Objectの概念の肝である、「生成された瞬間に完成していること」を満たせます。


## Email, Passwordのクラスを定義
上記のabstract classを用いてEmailとPasswordのValue Objectを定義します。

```typescript
class Email extends ValueObject<"Email",string> {
  constructor(readonly value: string) {
    super(value)
  }

  protected isValid(): boolean {
    const regexp = /^[a-zA-Z0-9_+-]+(\.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
    return regexp.test(this.value)
  }
}

class Password extends ValueObject<"Password",string> {
  constructor(readonly value: string) {
    super(value)
  }

  protected isValid(): boolean {
    console.log(this.value)
    return this.value.length >= 8
  }
}

```
(パスワードのバリデーションはとりあえず文字数カウントだけチェックしています。)
こうすることにより、下記のような利用ができない状態で、各Value Objectをクラスとして扱えます。
```typescript
const email: Email = new Password("maaaa") 👉 エラーになる
```

## バリデーションエラーはnullにしたい

上記でValue Objectとしての要件は満たせました。
しかし、バリデーションエラーに引っ掛かるとエラーがスローされるので、扱いが少々面倒です。

```typescript
try {
    const email = new Email("hoge")
} catch(e) {
    // TODO: handling
}
```

クラスの生成箇所でのエラーキャッチがOKであれば、このままでOKです。
ちょっと不便に感じる場合は、バリデーションに失敗したらnullが返ってくるような構造にします。

下記のようなメソッドを各クラスに仕込みます。

```typescript
  static instantiate(value: string): Email | null  {
    try {
      const newInstance = new Email(value)
      return newInstance
    } catch(e) {
      return null
    }
  }
```

バリデーションエラーのスローはコンストラクタ内で行われます。
インスタンス生成処理をstaticメソッドで担い、メソッド内部でエラーをキャッチします。
エラーだった場合はnullを返します。

利用方法は下記の通りです。
```typescript
const email = Email.instantiate("example@com")
```

```typescript
const email = Email.instantiate("example") // null
```

全体コードです。

```typescript
class Email extends ValueObject<"Email",string> {
  constructor(readonly value: string) {
    super(value)
  }

  static instantiate(value: string): Email | null  {
    try {
      const newInstance = new Email(value)
      return newInstance
    } catch(e) {
      return null
    }
  }

  protected isValid(): boolean {
    const regexp = /^[a-zA-Z0-9_+-]+(\.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
    return regexp.test(this.value)
  }
}

class Password extends ValueObject<"Password",string> {
  constructor(readonly value: string) {
    super(value)
  }

  static instantiate(value: string): Password | null  {
    try {
      const newInstance = new Password(value)
      return newInstance
    } catch(e) {
      return null
    }
  }

  protected isValid(): boolean {
    console.log(this.value)
    return this.value.length >= 8
  }
}
```
以上です。もう少し良いTipsあれば随時アップデートしていきます。

# 参考資料

- https://gist.github.com/okunokentaro/dedacc8f57c2c35746e2aab8032864ee
- https://blog.mamansoft.net/2020/02/19/express-value-object-by-typescript/
- https://qiita.com/rokumura7/items/2f68643cd5086917037a
- https://khalilstemmler.com/articles/typescript-value-object/