Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Field becomes optional when value is any or unknown #1628

Open
jasonkuhrt opened this issue Dec 3, 2022 · 15 comments
Open

Field becomes optional when value is any or unknown #1628

jasonkuhrt opened this issue Dec 3, 2022 · 15 comments
Labels
enhancement New feature or request

Comments

@jasonkuhrt
Copy link

const s = z.object({ x: z.unknown() })
type s = z.infer<typeof s>
// s has { x?: unknown } but should be { x: unknown }
@colinhacks
Copy link
Owner

colinhacks commented Dec 4, 2022

I'd say this is intentional for the moment. Currently Zod can't really differentiate between {arg: string | undefined} and {arg?: string | undefined}. When you pass a key schema S s.t. undefined extends infer<typeof S>, Zod will add the question mark. In this case undefined extends unknown (because everything does), hence the question mark. This also agrees with runtime behavior:

const schema = z.object({ x: z.unknown() });
schema.parse({}); // works

The broader question of how/whether to provide an API that differentiates between different types of optionality is tracked here: #635

@jasonkuhrt
Copy link
Author

How does one write anything but something must be passed in and it cannot be undefined?

@dilame
Copy link

dilame commented Dec 5, 2022

I came up to this object schema definition

export type ZodObjectIdentity<T extends z.ZodRawShape, Side extends '_input' | '_output'> = {
  [K in keyof T]: T[K][Side];
};

export function zodObjectIdentity<T extends z.ZodRawShape>(
  t: T,
): z.ZodObject<T, 'strip', z.ZodTypeAny, ZodObjectIdentity<T, '_output'>, ZodObjectIdentity<T, '_input'>> {
  // @ts-ignore
  return z.object(t);
}

It removes question marks magic, so you can use it like this

const s = zodObjectIdentity({ x: z.unknown() })
type s = z.infer<typeof s>
//^? { x: unknown }

@lostpebble
Copy link

I am also running into this issue- as I'm creating a generic parser for a first pass on some inputs and one of the properties just has to be an object- but we don't need to know what that object looks like yet.

Right now, it seems like there's no way for us to define a schema like so:

interface ISchemaExample {
  inputs: object;
  otherProperty: string;
}

Where inputs is a required object type. If I try and use:

z.object()

It always requires that I give it a specific shape.

It would be really nice if we could define "any object" as a property (and it is a required property when it does schema validation).

@colinhacks
Copy link
Owner

How does one write anything but something must be passed in and it cannot be undefined?

This is the best I've got:

const schema = z.object({
    field: z.custom((x) => x !== undefined),
  });

@colinhacks
Copy link
Owner

It would be really nice if we could define "any object" as a property (and it is a required property when it does schema validation).

Cool idea! Open to this as a PR. Just setting the "default shape" to {} should work, though you'll end up with an empty object every time unless you use .passthrough().

z.object().passthrough()

@ranmocy
Copy link

ranmocy commented Dec 11, 2022

If you guys like me need to make sure the return type is compatible with existing interfaces, then you could do something like this:

object({
  field: custom<MyType>((val) => val !== undefined && val !== null),
})
// Or
object({
  field: object({}).passthrough().refine<MyType>((val): val is MyType => val !== undefined && val !== null),
})

@lostpebble
Copy link

Cool idea! Open to this as a PR. Just setting the "default shape" to {} should work, though you'll end up with an empty object every time unless you use .passthrough().

I've just added a PR which pretty much does exactly that (makes the shape an empty object and set the object to "passthrough". Its added as the type z.anyObject().

@igalklebanov
Copy link
Contributor

igalklebanov commented Dec 12, 2022

I've just added a PR which pretty much does exactly that (makes the shape an empty object and set the object to "passthrough". Its added as the type z.anyObject().

Isn't that the purpose of z.record?

Possible today:

z.record(z.unknown()) // don't care about key type.

Proposal:

z.record() // don't care about anything.

Intuitively, when writing typescript, you'd use Record<string, any> or Record<string, unknown> for such use case.

@santosmarco-caribou
Copy link
Contributor

Cool idea! Open to this as a PR. Just setting the "default shape" to {} should work, though you'll end up with an empty object every time unless you use .passthrough().

I've just added a PR which pretty much does exactly that (makes the shape an empty object and set the object to "passthrough". Its added as the type z.anyObject().

What does it infer to? I'm almost certain it will infer to {}, which is the same as unknown and this will not correctly resemble empty objects. Actually, everything that extends an object will pass this from the inference point of view.

Of course, in reality, Zod is smart enough to let only objects pass. But if you then use this Schema as a function parameter validator, for example, Zod will parse the argument correctly, but TypeScript will not complain if you pass it a 2.

There are some things we can do to solve this:

  1. Add a check on the type that generates the ZodObject output to check if the output is {}, in which case it should transform it to Record<string, never>—but I don't like this very much because we already have Record schemas.

  2. You could intersection the empty ZodObject with ZodBrand so that it has a brand that will not interfere on the parsing, but now TS knows that I can't pass a 2 to a function argument.

@lostpebble
Copy link

lostpebble commented Dec 13, 2022

What does it infer to? I'm almost certain it will infer to {}, which is the same as unknown and this will not correctly resemble empty objects. Actually, everything that extends an object will pass this from the inference point of view.

It infers to object - which is a standard TypeScript type for object and makes sense in this situation.

These type assertions seem to be correct to me:
image

In the code, I've made sure the "output" type is object in this case- so yea, its not quite the same as passing in {} and making it "passthrough"- there was a little more involved.

@santosmarco-caribou
Copy link
Contributor

Awesome! Thanks for looking into that. object does make sense.

@JacobWeisenburger
Copy link
Contributor

Has this been resolved? Is so, I'd like to close this issue.

@JacobWeisenburger JacobWeisenburger added the closeable? This might be ready for closing label Dec 26, 2022
@lostpebble
Copy link

Sorry for late response @JacobWeisenburger - but I don't think this has been resolved until #1675 gets merged.

@flex-hyuntae
Copy link

flex-hyuntae commented Oct 2, 2024

{ a: unknown } and { a?: unknwon} are different

type TestType = {
  test: unknown
}

const testValue: TestType = { test: 'value' }

const TestSchema = z.object({
  test: z.unknown(),
});

const parsedTestValue: TestType = TestSchema.parse(testValue);

Error
Type '{ test?: unknown; }' is not assignable to type 'TestType'. Property 'test' is optional in type '{ test?: unknown; }' but required in type 'TestType'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants