Giter Site home page Giter Site logo

Specify valid keys in Record about zod HOT 10 CLOSED

danenania avatar danenania commented on August 25, 2024 7
Specify valid keys in Record

from zod.

Comments (10)

danenania avatar danenania commented on August 25, 2024 7

Found a seemingly workable solution to this:

const ZodLiteralRecord = <
  KeyType extends string,
  ZodValueType extends z.ZodTypeAny
>(
  keys: KeyType[],
  zodValueType: ZodValueType
) =>
  z.object(
    keys.reduce(
      (agg, k) => ({
        ...agg,
        [k]: zodValueType.optional(),
      }),
      {} as Record<KeyType, z.ZodUnion<[ZodValueType, z.ZodUndefined]>>
    )
  );

Can be used like so:

const FlagsSchema = ZodLiteralRecord([<const>"flag1", <const>"flag2"], z.literal(true));
type Flags = z.infer<typeof FlagsSchema>;  // {flag1?: true | undefined, flag2?: true | undefined}

Perhaps worth adding to the core?

from zod.

low-ghost avatar low-ghost commented on August 25, 2024 7

I was surprised to see that records with literals and zod enums work perfectly fine, as tests in this general area show

const recordWithEnumKeys = z.record(z.enum(["Tuna", "Salmon"]), z.string());
type recordWithEnumKeys = z.infer<typeof recordWithEnumKeys>;
const recordWithLiteralKeys = z.record(
z.union([z.literal("Tuna"), z.literal("Salmon")]),
z.string()
);
type recordWithLiteralKeys = z.infer<typeof recordWithLiteralKeys>;
so perhaps the bug directly above my comment is solved?

There's a couple of interesting things though. Records in ts enforce that all of their keys are present. e.g.:

enum Something {
  F1 = "f1",
  F2 = "f2",
}
type SomethingNums = Record<Something, number>;
const f1: AssertEqual<SomethingNums, { f1: number; f2: number }> = true;

Note that the number fields are required and not { f1?: number; f2?: number }. So the approach described again in @akomm's comment is not equivalent even in terms of the actual record parsing. So

const SomethingNumsZ = z.record(z.union([z.literal("f1"), z.literal("f2")]), z.number());
expect(() => SomethingNumsZ.parse({ f1: 1 })).to.throw("should throw due to missing f2");

fails to throw. That goes against my expectation at least. Same could be said of @danenania's solution, where the fields are explicitly and intentionally made optional.

In that line, I like this as a util:

export const RecordOf = <
  T extends Record<string, string>,
  ZodValueType extends z.ZodTypeAny
>(
  obj: T,
  zodValueType: ZodValueType
) => {
  type KeyType = T[keyof T];
  const keys = Object.values(obj);
  return z.object(
    keys.reduce(
      (agg, k) => ({
        ...agg,
        [k]: zodValueType,
      }),
      {} as Record<KeyType, ZodValueType>
    )
  );
}

and some tests to prove that RecordOf functions very similarly to ts's Record:

describe("RecordOf", () => {
  enum Ordinals {
    FIRST = "first",
    SECOND = "second",
    THIRD = "third"
  };

  const PlanetZ = z.object({
    name: z.string(),
    moons: z.number(),
  });

  // Well, at least the start of one
  const SolarSystemZ = RecordOf(Ordinals, PlanetZ);
  type SolarSystem = z.infer<typeof SolarSystemZ>;
  // The equivalent in pure ts. could infer from PlanetZ, but being explict/safe here
  type NativeSolarSystem = Record<Ordinals, { name: string; moons: number; }>;

  const testObj = {
    first: { name: "Mercury", moons: 0 },
    second: { name: "Venus", moons: 0 },
    third: { name: "Earth", moons: 1 },
  };

  it("should make a zod record type with provided keys and value", () => {
    // borrowed from zod's tests
    type AssertEqual<T, Expected> = [T] extends [Expected]
      ? [Expected] extends [T]
      ? true
      : false
      : false;
    const f1_: AssertEqual<NativeSolarSystem, SolarSystem> = true;
    // @ts-ignore _def doesn't have typeName on its type
    expect(SolarSystemZ._def.typeName).to.equal(
      "ZodObject",
      "should not be a ZodRecord, unfortunately, because we want to require all keys" 
    );
  });

  it("should parse a correct object", () => {
    expect(SolarSystemZ.parse(testObj)).to.deep.equal(testObj);
  });

  it("should throw on bad check", () => {
    // pull off 'first' to fail check. Note that this means the result requires all
    // enum fields to be present. use `.optional()` if that isn't intended
    const { first: _, ...restObj } = testObj;
    expect(() => SolarSystemZ.parse(restObj)).to.throw();
  });
});

as the last test says, could always SolarSystemZ.optional() to get the alternative behavior

from zod.

colinhacks avatar colinhacks commented on August 25, 2024 4

Consider implementing this yourself! There's nothing special about the schema types implemented in the core library, everything is just a subclass of ZodType. Use the implementation of ZodRecord as a model, it's only ~60 lines of code.

from zod.

scotttrinh avatar scotttrinh commented on August 25, 2024 1

There is a related feature of Record that I use all of the time, too. Requiring all of the keys from an enum to be in the record. Like this:

enum Foo {
  BAR = "BAR",
  BAZ = "BAZ"
}

type FooMap = Record<Foo, string>;

const valid: FooMap = {
  [Foo.BAR]: "bar",
  [Foo.BAZ]: "baz",
};

const invalid: FooMap = { // Type error here
  [Foo.BAR]: "bar",
};

https://www.typescriptlang.org/play?#code/KYOwrgtgBAYg9nKBvAUFKAhAggJSgXigCJsciAaNTLALQOOxqJQF8UUAXATwAdhYEAWQCGPejmABjOACcAJgB54cclADOHGQEsQAcwB8AbnbSQGqADdhAGy1yAXALgixhVOgDaygHSkAuo5EAEbCMhRUXgi+tAHEIQBe4SzGKKbmOla2Dk4u9O5QkXDROLHBoUmGQA

from zod.

akomm avatar akomm commented on August 25, 2024 1

@ByronBroughten

const keys = z.union([z.literal("flag1"), z.literal("flag2")])
const Flags = z.record(keys, z.boolean())

There seem to be a bug though. Parsing with zod does the job right, but infering is broken - partially:

type Flags = z.TypeOf<typeof Flags> // is infered as {flag1: boolean, flag2: boolean}
const keys = z.union([z.literal("flag1"), z.literal("flag2")])
const Flags = z.record(keys, z.boolean())

// this correctly passes
Flags.parse({flag1: true})
// but this fails
const flags: Flags = {flag1: true}

However the post did not sound like it was talking about bug, but missing feature, hence I said its there. I also only discovered the bug just now.

from zod.

colinhacks avatar colinhacks commented on August 25, 2024

Interesting proposal. I'm open to moving something like this into core. Leaving this open until I get around to doing so.

from zod.

privatenumber avatar privatenumber commented on August 25, 2024

I also have a similar need for providing a schema for record keys.

In my use-case, I have a schema where the keys must be a string of length 1.

Being able to do something like this would be the most intuitive:

z.record(z.string().length(1), z.boolean()); // where the key parameter for record only accepts string types

from zod.

akomm avatar akomm commented on August 25, 2024

Its implemented in 3.9.8 (maybe earlier, did not check). It was not there in 3.5.1

from zod.

ByronBroughten avatar ByronBroughten commented on August 25, 2024

Its implemented in 3.9.8 (maybe earlier, did not check). It was not there in 3.5.1

I checked the docs and didn't find anything on this. Will you show me how this feature is implemented in 3.9.8? Just so I can verify whether it's implemented in my current version (3.8.2).

I often use record types with unions that define required keys, for objects in which the value types are the same throughout. It's much more condensed and readable than defining each entry.

from zod.

stale avatar stale commented on August 25, 2024

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

from zod.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.