Branded record keys break generics when used in an object with other branded types
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.
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!
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
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?
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
Fixed in recent versions.