HCT Picker

Monochrome
Neutral
Tonal Spot
Vibrant
Expressive
Fidelity
Content
Rainbow
Fruit Salad
2021
2025
OK

Overview

Overview is generated by AI and may contain errors.

探討如何在 TypeScript 中基於 Result 模式與 DDD 值對象概念設計 Gender 類型——從資料庫表映射、智能枚舉到私有構造器與防腐校驗的完整實踐。

Expand
Copy Link

TypeScript 實踐 DDD 值對象:基於 Result 模式的 Gender 類型設計

在領域驅動設計(DDD)中,「值對象(Value Object)」是構建領域模型的核心基石之一。本文將以 Gender(性別)為例,探討如何使用 TypeScript 實現一個具備類型安全、防腐校驗以及富含領域行為的「智能枚舉(Smart Enum)」值對象。

为何不使用原始类型

假設使用原始類型(例如 string 類型)會導致語義缺失、不受限的取值和不受控的行爲。假設 Gender 值對象與數據庫值映射無法對齊時,原始類型的弊端會立刻顯現(通常發生在數據庫表性別字段發生變化)。

使用值對象構建 Gender 的優勢:

  • 不可變性
  • 數據校驗
  • 行爲封裝

關鍵設計

Gender 值對象對應數據庫中的一張表 genders ,表定義是:

create table `genders` (
    `value`        varchar(32) primary key,
    `display_name` varchar(64) not null
);

Gender 值對象包含如下字段(對應 SQL Schema):

  • value
  • displayName

以及如下常用智能枚舉字段:

  • Male
  • Female
  • Other
  • Unknown

期待用法:

// 通過智能枚舉獲取預定義數據
const male = Gender.Male

// 獲取 value 和 displayName 字段
const maleValue = male.value
const maleDisplayName = male.displayName

核心代碼實現

Gender 采用標准的領域糢型設計模式(智能枚舉、私有構造器、Result 模式)。代碼預期合法取值被寫死為 Male, Female, Other, Unknown 。

export interface IGenderConstructorArgs {
    readonly value: string
    readonly displayName: string
}

export class Gender {

    /* #region Properties */
    public readonly value: string
    public readonly displayName: string
    /* #endregion */

    /* #region Start Enums */
    public static readonly Male = new Gender({ value: 'male', displayName: 'Male' })
    public static readonly Female = new Gender({ value: 'female', displayName: 'Female' })
    public static readonly Other = new Gender({ value: 'other', displayName: 'Other' })
    public static readonly Unknown = new Gender({ value: 'unknown', displayName: 'Unknown' })
    /* #endregion */

    public static readonly All: readonly Gender[] = [
        Gender.Male,
        Gender.Female,
        Gender.Other,
        Gender.Unknown,
    ]

    /* #region Constructors */
    private constructor(args: IGenderConstructorArgs) {
        this.value = args.value
        this.displayName = args.displayName
    }
    public static From(args: IGenderConstructorArgs): IResultOfT<Gender, GenderError> {
        const match = Gender.All.find(g => g.value === args.value)
        if (match) {
            return ok(match)
        }

        return err(GenderError.InvalidValue(args.value))
    }
    /* #endregion */

    /** Returns the human-readable display name (e.g. `"Male"`, `"Female"`). */
    public toString(): string {
        return this.displayName
    }
}