有没有一种优雅的方式在打字稿中拥有值对象?

Max*_*hko 5 architecture domain-driven-design typescript

问题

出于验证目的,我希望在系统的核心中有一些值对象。这些值对象可以是类:

class UserName {
  readonly value: string;

  constructor(value: string) {
    this.value = value.trim();
    if (this.value === '') {
      throw new Error('Empty value');
    }
  }
}

// and use it

userService.updateName({id: 1, name: new UserName('Maxim')});
Run Code Online (Sandbox Code Playgroud)

但它效果不佳,因为每个人都可以在不实例化类的情况下调用该服务,因此无需验证:

userService.updateName({id: 1, name: {value: INVALID_NAME});
Run Code Online (Sandbox Code Playgroud)

我的解决方案

我有一个接口和实用函数来创建不方便手动创建的对象:

interface ValueObject<T extends string, U> {
  readonly type: T;
  readonly value: U;
  readonly DO_NOT_CREATE_MANUALLY: 'DO NOT CREATE THIS MANUALLY!',
}

function ValueObject<T extends string, U>(type: T, value: U): ValueObject<T, U> {
  return {
    type,
    value,
    DO_NOT_CREATE_MANUALLY: 'DO NOT CREATE THIS MANUALLY!',
  };
}
Run Code Online (Sandbox Code Playgroud)

我有一个值对象和实用函数的接口:

interface UserName extends ValueObject<'UserName', string> {}

function UserName(value: string): UserName {
  value = value.trim();
  if (value === '') {
    throw new Error('Empty value');
  }

  return ValueObject('UserName', value);
}
Run Code Online (Sandbox Code Playgroud)

我这样使用它:

userService.updateName({id: 1, name: UserName('Maxim')});
Run Code Online (Sandbox Code Playgroud)

效果很好。没有人会写:

userService.updateName({
  id: 1,
  name: {
    value: INVALI_VALUE,
    type: 'UserName',
    DO_NOT_CREATE_MANUALLY: 'DO NOT CREATE THIS MANUALLY!',
  },
});
Run Code Online (Sandbox Code Playgroud)

问题

有没有更优雅的方式在打字稿中拥有值对象而不需要DO_NOT_CREATE_MANUALLY

Kry*_*ten 5

为了进一步充实@titian 的评论......

考虑下面的类:

class UserName {
    public readonly value: string;
    constructor(value: string) {
        if (value === '') {
            throw new Error('Empty value');
        }
        this.value = value;
    }
}
Run Code Online (Sandbox Code Playgroud)

根据Typescript 类型兼容性文档,任何具有属性的对象都value: string将与此类的实例兼容。这就是您想要避免的 - 您想要确保UserName传递给的任何类似对象userService.updateName实际上都是一个UserName对象。

解决方案是向类添加私有属性,以便任何UserName类似对象也必须具有该私有属性:

class UserName {
    public readonly value: string;
    private readonly is_nominal: boolean = true;
    constructor(value: string) {
        if (value === '') {
            throw new Error('Empty value');
        }
        this.value = value;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果有人执行以下操作:

userService.updateName({ id: 1, value: { value: 'James T Kirk' } })`
Run Code Online (Sandbox Code Playgroud)

Typescript 会抱怨,因为{ value: 'James T Kirk' }缺少私有is_nominal财产。

然而,有一种更简单(恕我直言,更干净)的方法来添加私有字段:将值设为私有并添加访问器。

class UserName {
    private readonly _value: string;
    constructor(value: string) {
        if (value === '') {
            throw new Error('Empty value');
        }
        this._value = value;
    }

    get value() { return this._value; }
}
Run Code Online (Sandbox Code Playgroud)