zod icon indicating copy to clipboard operation
zod copied to clipboard

Branded record keys break generics when used in an object with other branded types

Open greg-sims opened this issue 1 year ago • 3 comments

Versions

Zod: 3.23.8 Typescript: 5.6.3

Observations

import { z } from 'zod';

function testFn<T>(zodType: z.ZodType<T>) {
  return zodType;
}

const branded_1 = z.string().brand('type1');
const branded_2 = z.string().brand('type2');
const bad_record = z.record(branded_1, z.string());

const good_1 = z.object({
  branded_1,
});
testFn(good_1); // No type errors, a branded_1 in an object is fine

const good_2 = z.object({
  branded_2,
});
testFn(good_2); // No type errors, branded_2 in an object is fine

const good_3 = z.object({
  branded_1,
  branded_2,
  record_1: z.record(branded_1, z.any()),
  record_2: z.record(branded_1, z.unknown()),
  record_3: z.record(branded_1, z.undefined()),
});
testFn(good_3); // No type errors, we can have branded records where the value is potentially undefined

const good_4 = z.object({
  bad_record,
});
testFn(good_4); // No type errors, bad_record is not bad by itself

const good_5 = z.object({
  x: z.number(),
  bad_record,
});
testFn(good_5); // No type errors, bad_record is not bad with some other non-branded types

const bad_1 = z.object({
  branded_1,
  bad_record,
});
testFn(bad_1); // Type error!, bad_record cannot be put with another branded type here

const bad_2 = z.object({
  branded_2,
  bad_record,
});
testFn(bad_2); // Type error! it doesn't matter if it's mixed with the branded type it uses, any other branded type will cause the issue

const bad_3 = z.object({
  a: z.object({
    branded_2,
  }),
  b: z.object({
    bad_record,
  }),
});
testFn(bad_3); // Type error! Issue still occurs when separately nested

Errors are:

mre.ts(45,8): error TS2345: Argument of type 'ZodObject<{ branded_1: ZodBranded<ZodString, "type1">; bad_record: ZodRecord<ZodBranded<ZodString, "type1">, ZodString>; }, "strip", ZodTypeAny, { ...; }, { ...; }>' is not assignable to parameter of type 'ZodType<{ branded_1: string & BRAND<"type1">; bad_record: Partial<Record<string & BRAND<"type1">, string>>; }, ZodTypeDef, { branded_1: string & BRAND<...>; bad_record: Partial<...>; }>'.
  The types of '_input.branded_1' are incompatible between these types.
    Type 'string' is not assignable to type 'string & BRAND<"type1">'.
      Type 'string' is not assignable to type 'BRAND<"type1">'.
mre.ts(51,8): error TS2345: Argument of type 'ZodObject<{ branded_2: ZodBranded<ZodString, "type2">; bad_record: ZodRecord<ZodBranded<ZodString, "type1">, ZodString>; }, "strip", ZodTypeAny, { ...; }, { ...; }>' is not assignable to parameter of type 'ZodType<{ branded_2: string & BRAND<"type2">; bad_record: Partial<Record<string & BRAND<"type1">, string>>; }, ZodTypeDef, { branded_2: string & BRAND<...>; bad_record: Partial<...>; }>'.
  The types of '_input.branded_2' are incompatible between these types.
    Type 'string' is not assignable to type 'string & BRAND<"type2">'.
      Type 'string' is not assignable to type 'BRAND<"type2">'.
mre.ts(61,8): error TS2345: Argument of type 'ZodObject<{ a: ZodObject<{ branded_2: ZodBranded<ZodString, "type2">; }, "strip", ZodTypeAny, { branded_2: string & BRAND<"type2">; }, { branded_2: string; }>; b: ZodObject<...>; }, "strip", ZodTypeAny, { ...; }, { ...; }>' is not assignable to parameter of type 'ZodType<{ a: { branded_2: string & BRAND<"type2">; }; b: { bad_record: Partial<Record<string & BRAND<"type1">, string>>; }; }, ZodTypeDef, { ...; }>'.
  The types of '_input.a.branded_2' are incompatible between these types.
    Type 'string' is not assignable to type 'string & BRAND<"type2">'.

Expected

testFn should take any instance of ZodType, which ZodObject extends, without error.

greg-sims avatar Oct 15 '24 16:10 greg-sims

Hi, @greg-sims. I'm Dosu, and I'm helping the Zod team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • The issue involves a type error when using branded record keys with other branded types in Zod and TypeScript.
  • You provided examples showing that branded records work independently or with non-branded types but fail with other branded types.
  • This seems to be a limitation in TypeScript's handling of branded types within generics.
  • There has been no activity or comments on this issue since it was opened.

Next Steps:

  • Please let me know if this issue is still relevant with the latest version of Zod. If so, you can keep the discussion open by commenting here.
  • Otherwise, the issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

dosubot[bot] avatar Jun 18 '25 16:06 dosubot[bot]

Hi, @greg-sims. I'm Dosu, and I'm helping the Zod team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • The issue involves a type error when using branded record keys with other branded types in Zod and TypeScript.
  • You provided examples showing that branded records work independently or with non-branded types but fail with other branded types.
  • This seems to be a limitation in TypeScript's handling of branded types within generics.
  • There has been no activity or comments on this issue since it was opened.

Next Steps:

  • Please let me know if this issue is still relevant with the latest version of Zod. If so, you can keep the discussion open by commenting here.
  • Otherwise, the issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

The issue is still relevant as of 3.25.67

greg-sims avatar Jun 19 '25 09:06 greg-sims

Hi @greg-sims, thanks for confirming that this issue is still relevant with version 3.25.67. @colinhacks, could you please assist with the type error involving branded record keys and other branded types in Zod?

dosubot[bot] avatar Jun 19 '25 09:06 dosubot[bot]

Hi @greg-sims! I'm Dosu and I’m helping the zod team.

Thanks for confirming this is still relevant. This type error with branded record keys and other branded types is a known TypeScript limitation, not a Zod bug. The Zod team has discussed this in detail—fixing it would require a nominal type system, which isn't planned due to the complexity involved (discussion).

In Zod 4, branding implementation changed, but the core TypeScript inference issue remains: TypeScript can't always track brands through generics or when combining branded types in objects or records (details, workarounds). Workarounds include providing explicit type annotations, avoiding generics for branding steps, or using type casts where necessary.

If you want to experiment with the new branding model, you can try Zod 4, but the TypeScript limitation still applies. If this answers your question, please close the issue; otherwise, let me know if you need more details or want to discuss specific workarounds.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

dosubot[bot] avatar Jul 21 '25 22:07 dosubot[bot]

Fixed in recent versions.

colinhacks avatar Jul 25 '25 07:07 colinhacks