【TypeScript】Value Object(値オブジェクト)を実装する

TypeScriptでDDDのValue Objectを実装したい。

1. 抽象クラスの実装

declare const opaqueSymbol: unique symbol;
export abstract class BaseValueObject<T extends string, K> {
private readonly [opaqueSymbol]: T;
readonly value: K;
constructor(value: K) {
if (!this.isValid(value)) {
throw new InvalidValueObjectError('不正な値です');
}
this.value = value;
}
equals(other: BaseValueObject<T, K>): boolean {
return this === other || this.value === other.value;
}
protected abstract isValid(value: K): boolean;
}
export class InvalidValueObjectError extends Error {
static {
this.prototype.name = 'InvalidValueObjectError';
}
}
  • symbolを利用して等価性を持たせる
  • コンストラクタ内で抽象メソッドisValidを呼び、初期化時点でその値オブジェクトの値がルールに沿っているかを判定

2. 値オブジェクトの実装

import { BaseValueObject } from './base-value-object';
export class Uuid extends BaseValueObject<'Uuid', string> {
protected isValid(value: string): boolean {
return isUUID(value);
}
}

先ほどの抽象クラスを継承し、例としてUUID用の値オブジェクトを実装してみた。

3. 値オブジェクトの利用

import { randomUUID } from 'crypto';
const uuid1 = new Uuid(randomUUID());
console.log(uuid1.value); // => xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
const uuid2 = new Uuid('foo'); // => InvalidValueObjectError: 不正な値です

余談

zodで使う場合

const uuidSchema = z.string().transform((v) => new Uuid(v));

TypeORMで使う場合

TypeORMのエンティティクラスで値オブジェクトの型を持つカラムを定義したい場合。

import { ValueTransformer } from 'typeorm';
import { BaseValueObject } from './base-value-object';
interface NewableClass<T> {
new (...args: any[]): T;
}
export function ValueObjectTransformer<T extends string, K>(
ValueObjectClass: NewableClass<BaseValueObject<T, K>>,
): ValueTransformer {
return {
from: (value: K): BaseValueObject<T, K> => new ValueObjectClass(value),
to: ({ value }: BaseValueObject<T, K>): K => value,
};
}

@Columnデコレーターのtransformerオプションで実現できるので、それに指定するオブジェクトを共通化すべくValueObjectTransformer関数を実装。

import { Column, Entity } from 'typeorm';
import { BaseEntity } from '../../common/base.entity';
import { Uuid } from './uuid.value-object';
import { ValueObjectTransformer } from '../../common/value-objects/value-object-transformer';
@Entity('xxx_entities')
export class xxxEntity {
@Column({
type: 'uuid',
transformer: ValueObjectTransformer(Uuid),
})
uuid: Uuid;
}

あとは対象カラムのtransformerに指定すればOK。これでSQLクエリの実行前後で素の値←→値オブジェクトの変換が行われる。

class-validatorで使う場合

NestJSにおけるDTOクラスなどでありがちな、リクエストとして受け取ったプリミティブ値を値オブジェクトに変換したい場合。

import { Allow } from 'class-validator';
import { Uuid } from './uuid.value-object';
import { Transform } from 'class-transformer';
export class xxxDTO {
@Allow()
@Transform(({ value }) => new Uuid(value), { toClassOnly: true })
readonly uuid: Uuid;
}

対象のプロパティに@Transformデコレータを付与することで実現できる。

また今回のUuidクラスのようにドメインルールの判定をコンストラクタ内で行っている場合、class-validatorによるバリデーションは必要ないので@Allowデコレータを付与する。

12

参考
  1. TypeScript での値オブジェクト実装

  2. relation calls @Column transformer even if relation is null · Issue #9739 · typeorm/typeorm