【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 Error(this.getErrorMessage());
}
this.value = value;
}
equals(other: BaseValueObject<T, K>): boolean {
return this === other || this.value === other.value;
}
protected abstract isValid(value: K): boolean;
protected abstract getErrorMessage(): string;
}
- symbolを利用して等価性を持たせる
- コンストラクタ内で抽象メソッド
isValid
を呼び、初期化時点でその値オブジェクトの値がルールに沿っているかを判定
2. 値オブジェクトの実装
import { BaseValueObject } from './base-value-object';
import { isUUID } from 'class-validator';
export class Uuid extends BaseValueObject<'Uuid', string> {
protected isValid(value: string): boolean {
return isUUID(value);
}
protected getErrorMessage(): string {
return 'UUIDの形式が不正です';
}
}
先ほどの抽象クラスを継承し、例として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'); // => Error: UUIDの形式が不正です
余談
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 { Token } from '../../token/token.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 { Token } from '../../token/token.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
デコレータを付与する。