Open nassero opened 1 year ago
I also just ran into this issue. intersection
in combination with seems to be flawed. The following code shouldn't even be accepted by typescript, but more importantly it just fails no matter what I pass to it.record
const first = z.record(
z.union([z.literal("one"), z.literal("two"), z.literal("three")]),
z.string(),
);
const second = z.record(
z.union([z.literal("ones"), z.literal("twos"), z.literal("threes")]),
z.string().array(),
);
const intersect = z.intersection(first, second);
console.log(intersect.parse({
one: ["example"],
}));
Just ran into the same issue with this type:
export interface Transform {
[schema: string]: {
[table: string]: boolean | RowTransform<RowShape>
}
}
const transformConfigOptionsSchema = z.object({
$mode: z
.union([z.literal('auto'), z.literal('strict'), z.literal('unsafe')])
.optional(),
$parseJson: z.boolean().optional(),
})
const transformConfigTableSchema = z.record(
z.string().describe('table'),
z.union([
z
.function()
.args(
z.object({ rowIndex: z.number(), row: z.record(z.string(), z.any()) })
)
.returns(z.record(z.string(), z.any())),
z.record(z.string(), z.any()),
])
)
export const transformConfigSchema = z.intersection(
transformConfigOptionsSchema,
z.record(
z
.string()
.refine((s) => s !== '$mode' && s !== '$parseJson')
.describe('schema'),
transformConfigTableSchema
)
)
export type TransformConfig = z.infer<typeof transformConfigSchema>
Expected to be able to parse this kind of object:
{
$mode: 'auto',
public: {
books: (ctx) => ({ title: 'A Long Story' }),
},
},
But it fails if I pass both the $mode
and public: {...}
parameters. However if I remove the $mode
, then it's working fine.
Also the type from the schema when infered seems correct:
type TransformConfig = {
$mode?: "auto" | "strict" | "unsafe" | undefined;
$parseJson?: boolean | undefined;
} & Record<string, Record<string, Record<string, any> | ((args_0: {
rowIndex: number;
row: Record<string, any>;
}, ...args_1: unknown[]) => Record<...>)>>
Edit:
I managed a workaround by using a custom validator:
const optionsKeys = ['$mode', '$parseJson']
const customTransformConfigValidator = (transformConfig: unknown) => {
if (typeof transformConfig !== 'object' || !transformConfig) {
throw new ZodError([
{
code: 'invalid_type',
expected: 'object',
received: transformConfig as any,
path: ['transform'],
fatal: true,
message: 'Transform is not a valid objet',
},
])
}
for (const [key, value] of Object.entries(
transformConfig as Record<string, unknown>
)) {
// Validate special keys
if (optionKeys.includes(key)) {
transformConfigOptionsSchema.parse({ [key]: value })
} else {
transformConfigTableSchema.parse({ [key]: value })
}
}
return true
}
const customTransformConfigSchema = z.custom<
z.infer<
typeof transformConfigOptionsSchema | typeof transformConfigTableSchema
>
>(customTransformConfigValidator)
export type TransformConfig = z.infer<typeof customTransformConfigSchema>
Facing the same problem, the inferred type is correct but fails when parsing. For example:
const CommonKeysSchema = z.object({
order: z.string().optional(),
sortBy: z.string().optional(),
})
const MappingFileSchema = z.intersection(
CommonKeysSchema,
z.record(z.nativeEnum(ENUM), AnotherSchema)
)
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
It is still a problem in 3.22.2.
import {z} from 'zod';
type SetType = 'a' | 'b' | 'c';
type OrigType = { [key in SetType]?: number[] } & { disabled?: boolean };
const SetTypeSchema = z.enum(['a', 'b', 'c']);
const TypeSchema = z.intersection(
z.record(SetTypeSchema, z.array(z.number())),
z.object({
disabled: z.boolean().optional()
})
);
type Type = z.infer<typeof TypeSchema>;
const a: OrigType = {a: [1]};
const zA: Type = a; // ok
TypeSchema.parse(a); // ok
console.log('a OK');
const b: OrigType = {b: [1], disabled: true};
const zB: Type = b; // ok
TypeSchema.parse(b); // runtime error: `Invalid enum value. Expected 'a' | 'b' | 'c', received 'disabled'` and `Expected array, received boolean`
console.log('b OK');
Type in my case looks correct, but I am not sure if z.record
should really be making all fields optional by default (Partial
). Copied from IDE:
type Type = Partial<Record<"a" | "b" | "c", number[]>> & { disabled?: boolean | undefined; }
I'm having a similar issue with 3.22, basically I have an object with a uids
key with is an array of string containing the ids of the entities which also are keys in the same object (yeah I agree that kinda sucks but that's the shape of an api I don't control). Basically, my desired type is:
type MyObject = { uids: string[] } & Record<string, { uid: string }>
I can create this schema:
const result = z.intersection(
z.record(
z.object({
uid: z.string(),
})
),
z.object({
uids: z.array(z.string()),
})
);
Which z.infer
tells me is:
type Result = Record<string, {
uid: string;
}> & {
uids: string[];
}
And the result is inferred correctly in vscode:
But when you run the code, the parsing fails because it tries to parse both types for all keys:
ZodError: [
{
"code": "invalid_type",
"expected": "object",
"received": "array",
"path": [
"result",
"uids"
],
"message": "Expected object, received array"
}
]
As of right now I haven't found a workaround.
Ok, so not sure if this helps anyone but I have been painfully trying to deal with this type of error myself
"message": "Expected object, received array"
which stems from using the z.record() approach above on my schema to help include some dynamic keys.
My example comes from using React-Hook-Form and trying to connect my dynamic input to the dynamic schema key. For some reason, if I give it a small tag before the string() key it can correctly infer the type and everything pulls in correctly.
eg. When registering my RHF input field that is deeply nested
const deeplyNestedSchema = z.object({ first: z.string(), year_amounts: z.record(z.string(), z.coerce.number()) })
{...register(first.second.${index}.year_amounts.${dynamicKey}} this doesnt work...
{...register(first.second.${index}.yearamounts.**tag${dynamicKey}} this does** work...
Not fully sure why this works and the other approach causes so much pain but its helped move my project along. For context, had tried intersection / .and / .transform etc
I have an intersection between a
z.object()
and az.record()
that has a refined key. Parsing an input fails since it seems to require all the input keys to match the record and the object, despite typescript seeing it as valid against the type.version: 3.20.6
Example: