question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[PROPOSAL] Flavoured and branded types

See original GitHub issue

Yes, I know about https://github.com/colinhacks/zod/issues/3 but I couldn’t find anything saying that you didn’t want to support nominal types in Zod 😄

Type alias

A type alias is just a name given to an already existing type. These won’t prevent assigning values between types.

// Those three types are all number underneath
type Age = number;
type DistanceInMeters = number;
type DistanceInMiles = number;

const age = 10;
const distanceInMeters = age;
const distanceInMiles = distanceInMeters;

// Those two types are all string underneath
type Name = string;
type DateISO8601 = string;

const name = "Zod";
const date = name;

// Easy to do with Zod
type TownName = z.string();

Tag

A tag is a property in a type that is used to differentiate it from other types. They are used to create discriminated unions.

type Shape = {
  area: number;
} & ({ radius: number } | { length: number });

// distinguishing between a Circle and a Square is annoying

declare const shape: Shape;

if ("radius" in shape) {
  console.log("The radius is " + shape.radius);
} else {
  console.log("The length is " + shape.length);
}

// and you can't use a switch or a Record to handle all the types of shapes
// which is why tags are useful

type Animal =
  | {
      tag: "dog";
      barkStrength: number;
    }
  | {
      tag: "cat";
      purrStrength: number;
    };

declare const animal: Animal;

switch (animal.tag) {
  case "dog":
    // you can use barkStrength here
  case "cat":
    // you can use purrStrength here
}

// Easy to do with Zod
const shapeSchema = z.union([
  z.object({
    tag: z.literal("circle"),
    radius: z.number(),
  }),
  z.object({
    tag: z.literal("square"),
    length: z.number(),
  }),
]);

Flavoured types

Sometimes you don’t want types to be interchangeable, such as distanceInMeter and distanceInMiles, so you could use flavoured type to prevent this. A flavoured type is a type with tag to prevent mixing flavoured types between themselves

type DistanceInMeter = number & { readonly brand?: unique symbol };
type DistanceInMiles = number & { readonly brand?: unique symbol };
const distanceInMeter: DistanceInMeter = 10; // this works
const distanceInMiles: DistanceInMiles = distanceInMeter; // this won't compile

You can’t do this with Zod, you have to create a type and have your Zod schema use this type

type Flavor<T,  FlavorT> = T & { _type?: FlavorT; };
type UUID = Flavor<string, 'UUID'>;
const uuid: z.Schema<UUID> = z.string().uuid() as any;

It would be easier just to be able to use Zod to do this

const uuidSchema  = z.string().uuid().flavour<"UUID">();
type UUID = z.infer<typeof uuidSchema>;
const uuid: UUID = "123e4567-e89b-12d3-a456-426614174000";

The code shouldn’t be too complex too

export interface ZodFlavourDef<T extends ZodTypeAny> extends ZodTypeDef {
  innerType: T;
  typeName: "ZodFlavour";
}

export type ZodFlavourType<
  B extends string,
  T extends ZodTypeAny = ZodTypeAny
> = ZodNominal<B, T>;

type ZodFlavourOutput<
  B extends string,
  T extends ZodTypeAny = ZodTypeAny
> = T["_output"] & { readonly __flavour?: B };

export class ZodFlavour<
  B extends string,
  T extends ZodTypeAny = ZodTypeAny
> extends ZodType<ZodFlavourOutput<B, T>, ZodFlavourDef<T>, T["_input"]> {
  _parse(
    ctx: ParseContext,
    data: any,
    parsedType: ZodParsedType
  ): ParseReturnType<T["_output"]> {
    return this._def.innerType._parse(ctx, data, parsedType);
  }

  unwrap() {
    return this._def.innerType;
  }

  static create = <B extends string, T extends ZodTypeAny = ZodTypeAny>(
    type: T,
    params?: RawCreateParams
  ): ZodFlavour<B, T> => {
    return new ZodFlavour({
      innerType: type,
      typeName: "ZodFlavour",
      ...processCreateParams(params),
    }) as any;
  };
}

Branded types

These are the same as flavoured types but they don’t accept primitive and have to be build using parse for example

const uuidSchema  = z.string().uuid().brand<"UUID">();
type UUID = z.infer<typeof uuidSchema>;
const uuid: UUID = "123e4567-e89b-12d3-a456-426614174000"; // won't compile
const uuid2: UUID = uuidSchema.parse("123e4567-e89b-12d3-a456-426614174000");

It would be the same code as above except for the ?

type ZodBrandOutput<
  B extends string,
  T extends ZodTypeAny = ZodTypeAny
> = T["_output"] & { readonly __brand: B };

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:10
  • Comments:8

github_iconTop GitHub Comments

7reactions
lazytypecommented, Nov 6, 2021

I think it’s pretty trivial to use branded types with zod using refinement:

export type UUID = Brand<'UUID', string>;
export function isValidUUID(id: string): id is UUID { 
  //                 Note the type guard ^^^^^^^^^^
  return UUIDRegExp.test(id);
}
export const UUIDSchema = z.string().refine(isValidUUID);

const uuid: UUID = UUIDSchema.parse('682a8518-a062-4078-9ae0-4b8184e5fe00')

Since there are different methods of creating branded types, e.g. intersecting with enums, unique symbols, etc, I think it’s preferable to leave that up to the user rather than zod choosing one for you.

4reactions
renkecommented, Jan 3, 2022

For what it is worth, I’ve created a small proof of concept library (dubbed vod) for usage in my private projects that provides validated value objects based on zod. It’s not something I would suggest to be used in production (yet?) but it might serve as source of inspiration. In addition to using the branded type approach it also employs a few other tricks to avoid being able to create value objects that have not been validated (such as using spreading to create a new object).

Read more comments on GitHub >

github_iconTop Results From Across the Web

Branding & Flavoring | Bright Inventions
Branding concept is a technique in which we add a unique field which will make our type differ from another types.
Read more >
Branded types · Issue #3 · colinhacks/zod - GitHub
This issue can be the primary discussion ground for implemented branded types. Option 1: Replicating io-ts's symbol trickery to create types ...
Read more >
21 Examples of Successful Co-Branding Partnerships (And ...
Need inspiration for your next brand partnership? Check out these real-world examples of great co-branding partnerships — and what makes ...
Read more >
Flavoring: Flexible Nominal Typing for TypeScript - Atomic Spin
TypeScript flavoring allows unbranded values to be implicitly converted into the branded type, but doesn't allow implicit conversion between ...
Read more >
How to write an Influencer Marketing proposal that gets ...
Brands will have a flavour of who you are from your online presence but using your proposal to introduce yourself gives you the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found