#How can I design this better?

37 messages · Page 1 of 1 (latest)

peak trail
#

I have a database table with some values intended as constants:

  • id: number
  • type: string
  • name: string
  • hex: string (#000000)

Where id is used as a primary/foreign key reference;
and type is generally just an UPPER_SNAKE_CASE version of name.

I'm trying to write a class to represent the constant type, but I also want it to have some kind of static/enum type for simpler usage like Color.RED. How can I design this better?


enum ColorEnum {
    RED = 1,
    GREEN = 2,
    BLUE = 3
}

class Color<Type extends Exclude<keyof typeof ColorEnum, number>> {
    
    public type: Type;
    public name: string;
    public readonly hex: `#${string}`;
    
    private constructor(type: Type, name: string, hex: `#${string}`) {
        this.type = type;
        this.name = name;
        this.hex = hex;
    }

    public static RED   = new Color(ColorEnum[1], "Red",    "#FF0000");
    public static GREEN = new Color(ColorEnum[2], "Green",  "#00FF00");
    public static BLUE  = new Color(ColorEnum[3], "Blue",   "#0000FF");

}

I did ask ChatGPT, just for some quick ideas, and it suggested something like this which might be fine

const ColorEnum = {
  RED: 1,
  GREEN: 2,
  BLUE: 3,
} as const;

type ColorEnum = typeof ColorEnum[keyof typeof ColorEnum]; // 1 | 2 | 3

class Color {
  public readonly id: ColorEnum;
  public readonly type: keyof typeof ColorEnum;
  public readonly name: string;
  public readonly hex: `#${string}`;

  private constructor(id: ColorEnum, type: keyof typeof ColorEnum, name: string, hex: `#${string}`) {
    this.id = id;
    this.type = type;
    this.name = name;
    this.hex = hex;
  }

  static readonly RED   = new Color(ColorEnum.RED, "RED",   "Red",   "#FF0000");
  static readonly GREEN = new Color(ColorEnum.GREEN, "GREEN", "Green", "#00FF00");
  static readonly BLUE  = new Color(ColorEnum.BLUE, "BLUE",  "Blue",  "#0000FF");

  static values(): Color[] {
    return [Color.RED, Color.GREEN, Color.BLUE];
  }
}
copper chasm
#

fwiw, you could store the color as a number rather than a string

#
class Color<Type extends Exclude<keyof typeof ColorEnum, number>> {
    public readonly value: number;
    
    private constructor(type: Type, name: string, value: number) {
        this.value = value;
    }

    public static RED   = new Color(ColorEnum[1], "Red",    0xFF0000);
    public static GREEN = new Color(ColorEnum[2], "Green",  0x00FF00);
    public static BLUE  = new Color(ColorEnum[3], "Blue",   0x0000FF);

}
```would be something like that (excluding unchanged parts)
#

not saying this is better per-se, but something to consider

#

why do you need both type and name?

#

that ChatGPT suggestion is.. yikes

peak trail
#

It is? lol

copper chasm
#

it's so excessive

peak trail
peak trail
copper chasm
torn venture
#

i'm confused by this API. is Color meant to be closed (nobody except you can create new colors) or open (i can do const magenta = new Color(someIDThatICreated, "Magenta", "#ff00ff") in my own code)?

#

basically my confusion boils down to: why is there only a fixed set of IDs?

peak trail
torn venture
#

but if it's in the database it can change at runtime, right? someone could go in and add a new color there?

copper chasm
#

right now it seems kinda like a mix between a struct and an enum so it kinda contradicts itself

peak trail
copper chasm
#

so this should be an enum kind of deal?

#

ah, the private constructor makes sense now.

#

i still don't see why you need both type and name though

peak trail
torn venture
copper chasm
#

then you have object identity to handle the "enum"ness, you don't need the backing enum

peak trail
torn venture
peak trail
copper chasm
#

this could just be like

export const Color = {
  RED: { name: "Red", value: 0xFF0000 },
  GREEN: { name: "Green", value: 0x00FF00 },
  BLUE: { name: "Blue", value: 0x0000FF },
};
torn venture
#

yeah, that's basically what i was just typing up ☝️

tough thicketBOT
#
mkantor#0

Preview:ts const Color = { RED: { type: 1, name: "Red", hex: "#FF0000", }, GREEN: { type: 2, name: "Green", hex: "#00FF00", }, BLUE: { type: 3, name: "Blue", hex: "#0000FF", }, } as const

torn venture
#

i would start with that, and if you want sanity checks like "nobody accidentally reused a type ID" then add them atop this basic structure

peak trail
#

With that approach, how would I assert a type of Color like const color: Color = ...

#

Oh

type Color = (typeof Color)[keyof typeof Color];
const asd: Color = Color.BLUE
tough thicketBOT
#
const Color = {
  RED: {
    type: 1,
    name: "Red",
    hex: "#FF0000",
  },
  GREEN: {
    type: 2,
    name: "Green",
    hex: "#00FF00",
  },
  BLUE: {
    type: 3,
    name: "Blue",
    hex: "#0000FF",
  },
} as const

type Color = typeof Color[keyof typeof Color]
//   ^? - type Color = {
//       readonly type: 1;
//       readonly name: "Red";
//       readonly hex: "#FF0000";
//   } | {
//       readonly type: 2;
//       readonly name: "Green";
//       readonly hex: "#00FF00";
//   } | {
//       readonly type: 3;
//       readonly name: "Blue";
//       readonly hex: "#0000FF";
//   }
torn venture
#

stepping back i would probably try to see if there's an easy way to get a single source of truth. maybe static codegen from your database, or have this object created from a database query when your program starts up, or have the source of truth in code and use this to populate your database

#

unless this never actually changes, then the duplication is no big deal

peak trail
#

They will very rarely change, if ever, but just in case lol