user17298649
user17298649

Reputation:

How to extract a single type from a Zod union type?

I'm using Zod and have an array containing different objects using a union. After parsing it I want to iterate through each item and extract it's "real" type / cut off the other types.

When checking for specific object properties, the following code works fine:

const objectWithNumber = zod.object({ num: zod.number() });
const objectWithBoolean = zod.object({ isTruthy: zod.boolean() });
const myArray = zod.array(zod.union([objectWithNumber, objectWithBoolean]));
const parsedArray = myArray.parse([{ isTruthy: true }, { num: 3 }]);

parsedArray.forEach((item) => {
  if ("num" in item) {
    console.info('objectWithNumber:', item);
    // TS knows about it => syntax support for objectWithNumber
  } else if ("isTruthy" in item) {
    console.info('objectWithBoolean:', item);
    // TS knows about it => syntax support for objectWithBoolean
  } else {
    console.error('unknown');
  }
});

An alternative would be using discriminated unions for this

const objectWithNumber = zod.object({ type: zod.literal("objectWithNumber"), num: zod.number() });
const objectWithBoolean = zod.object({ type: zod.literal("objectWithBoolean"), isTruthy: zod.boolean() });
const myArray = zod.array(zod.discriminatedUnion("type", [ objectWithNumber, objectWithBoolean ]));
const parsedArray = myArray.parse([{ type: "objectWithBoolean", isTruthy: true }, { type: "objectWithNumber", num: 3 }]);

parsedArray.forEach(item => {
  if (item.type === "objectWithNumber") {
    console.info('objectWithNumber:', item);
    // TS knows about it => syntax support for objectWithNumber
  } else if (item.type === "objectWithBoolean") {
    console.info('objectWithBoolean:', item);
    // TS knows about it => syntax support for objectWithBoolean
  } else {
    console.error('unknown');
  }
});

but I think I misunderstood this concept because there is just more code to write ( I can always add a shared property and inspect that one ). Any help on this is much appreciated :)

Are there better ways to identify a specific schema?

Upvotes: 4

Views: 16702

Answers (1)

robak86
robak86

Reputation: 368

If I understood you correctly, your question boils down to "Why one should use discriminated unions instead of shared fields combined with optional fields". (zod.js just lifts this concept into runtime providing validation functionality). In your example, there is in fact no reason to use a discriminator (type property), because each object holds only a single non-optional property that is mutually exclusive between types and can be easily used to distinguish types. However, the main issue with this code is that the object names (or shapes/structures) do not communicate any intentions - that's why it's difficult to see the benefits of discriminated unions.

You can think of discriminated union as a type that describes a family of objects and provides an unified mechanism (in a form of a discriminator property) to identify them. This approach is less fragile than checking for the existence of some manually picked properties (like for instance num property). What if num for any reason becomes optional? Then your check for num existence will break. Another argument for discriminated union is to decrease the number of optional properties. Compare two following examples:

// shared and optional fields instead of discriminated union
type Vehicle = {
  name: string;
  combustionEngine?: PetrolEngine | DieselEngine;
  tankCapacity?: number;
  electricEngine?: ElectricEngine;
  batteryCapacity?: number;
}
type Vehicle = 
  | GasolineCar
  | ElectricCar

type = GasolineCar {
  kind: "gasolineCar";
  name: string;
  engine: PetrolEngine | DieselEngine;
  tankCapacity: number;
}

type ElectricCar  = {
  kind: "electricCar"
  name: string;
  engine: ElecticEngine;
  batteryCapacity: number;
}

The example with discriminated union produces much more descriptive code. You don't have to add multiple checks for optional fields - instead, just identify (as early as possible) type by discriminator and pass the object to a function/method accepting the more narrow type (GasolineCar or ElectricCar instead of Vehicle).

Upvotes: 1

Related Questions