ARTICLE AD BOX
I realized today that branded types used as record (or Map) keys are not safe to use if you fail to pass the exact record shape.
Here's an example:
type KeyA = string & { readonly __branded__: unique symbol } type KeyB = string & { readonly __branded__: unique symbol } type Stats = { count: number }; type ObjA = Record<KeyA, Stats>; type ObjB = Record<KeyB, Record<KeyA, Stats>>; const a: ObjA = {} as any as ObjA; const b: ObjB = {} as any as ObjB; const c: ObjB = a; // no errorThe same happens with Zod branded types:
import * as z from "zod"; const keyASchema = z.string().brand<"keyA">(); const keyBSchema = z.string().brand<"keyB">(); type KeyA = z.infer<typeof keyASchema>; type KeyB = z.infer<typeof keyBSchema>; const statsSchema = z.object({ count: z.number(), }); const objA = z.record(keyASchema, statsSchema); const objB = z.record(keyASchema, z.record(keyBSchema, statsSchema)); type ObjA = z.infer<typeof objA>; type ObjB = z.infer<typeof objB>; const a: ObjA = {} as any as ObjA; const b: ObjB = {} as any as ObjB; const c: ObjB = a; // no errorThis does not happen with a regular string as key:
type KeyA = string type KeyB = string type Stats = { count: number }; type ObjA = Record<KeyA, Stats>; type ObjB = Record<KeyB, Record<KeyA, Stats>>; const a: ObjA = {} as any as ObjA; const b: ObjB = {} as any as ObjB; const c: ObjB = a; // failsWhat I think happens is that Typescript assumes that KeyA and KeyB are mutually exclusive since different types, so a code handling a ObjB will never try a KeyA. So if you give a ObjA no error will happen.
That's true, but I'd ideally have typescript giving me an error like: Expected a Record<KeyA, Record<KeyB, Stats>> and got Record<KeyA, Stats> instead
Any advice?
