schemix icon indicating copy to clipboard operation
schemix copied to clipboard

Additional type safety for enum default values

Open zygopleural opened this issue 2 years ago • 3 comments

Given an enum

const PlanType = createEnum((PlanType) => {
  PlanType
    .addValue("FREE")
    .addValue("PREMIUM")
})

and a model

const Plan = createModel((Plan) => {
  Plan
    .string("name", { id: true })
    .enum("type", PlanType, { default: "FREE" })
})

I want to ensure that the value passed as the default argument for the enum (e.g. { default: "FREE" }) is a valid value of the enum (i.e. ["FREE", "PREMIUM"]).

At the moment I am able to pass anything (e.g. .enum("type", PlanType, { default: "SOMETHING" })).

zygopleural avatar Sep 30 '22 10:09 zygopleural

FWIW I had a little play with this and it ends up looking like

export class PrismaEnum<Values = never> {
  private enumMap: Map<string, string | undefined> = new Map();

  constructor(public readonly name: string) {}

  public addValue<Values, Value extends string>(
    this: PrismaEnum<Values>,
    value: Value,
    options?: PrismaEnumOptions
  ): PrismaEnum<Values | Value> {
    // We need to return a new instance with a cloned `enumMap` 
    // for the types to do what we want.
    // If we modify `enumMap` in place then the type of `this` will be "wrong".
    const clone = new PrismaEnum(this.name);
    clone.enumMap = new Map(this.enumMap);
    clone.enumMap.set(value, options?.map);
    return clone;
  }

  // ...etc
}

Which then gives you typing like

// e.g.
const test: PrismaEnum<"foo" | "baz"> = new PrismaEnum("test")
  .addValue("foo")
  .addValue("baz");

Although this doesn't fit nicely with the create* functions where the callback is expected to mutate the values in place...

jmackie avatar Sep 30 '22 16:09 jmackie

Are we looking for Intellisense/Type-checking, or runtime checks?

Since, as @jmackie specified, the values are altered in place we'd have to go a roundabout way of making the values available to the TypeScript language server. One way would be to return the value of the alteration and build the strings onto the enum constructor type. Otherwise, we could have it default to "string" as we currently do.

ridafkih avatar Sep 30 '22 18:09 ridafkih

Another way would be supporting as such.

const PlanType = createEnum<"foo", "baz">((PlanType) => {
  PlanType
    .addValue("foo")
    .addValue("baz")
})

A little verbose, though.

ridafkih avatar Sep 30 '22 18:09 ridafkih