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

Standardize behavior of optionals + defaults #421

Merged
merged 5 commits into from
May 3, 2021
Merged

Standardize behavior of optionals + defaults #421

merged 5 commits into from
May 3, 2021

Conversation

colinhacks
Copy link
Owner

@colinhacks colinhacks commented May 2, 2021

1. Default values are now implemented in separate ZodDefault class.

Previously this logic was folded into ZodOptional.

2. Standardized behavior of .optional() and .nullable() methods.

Previously calling .optional() on an instance of ZodOptional instance would return the current instance instead of redundantly nesting ZodOptional instances. This seems entirely unnecessary, feels too magical, and introduced complexity into the typings. This complexity makes it harder to write generic functions on top of Zod: #355 (comment)

3. No undefined defaults

// before this was OK
z.string().optional().default(undefined)

// now it's not
z.string().optional().default('asdf'); // must be string

There's no point in setting undefined value as a default value, it's a NOOP. This also lets us explicitly eliminate optionals from the inferred output types of ZodDefault:

export class ZodDefault<T extends ZodTypeAny> extends ZodType<
  util.noUndefined<T["_output"]>,
  ZodDefaultDef<T>,
  T["_input"] | undefined
> {
  // ...
}

4. Calls to .optional always return schema with T | undefined

This is a consequence of #2. Previously calling .optional() on z.string().default() would return the instance unchanged, since defaults values were implemented in the ZodOptional class. Now it introduces a new ZodOptional instance that wraps a ZodDefault instance. As such the type signature is now string | undefined instead of string.

This introduces a nice duality into chained calls to default and optional.

z.string().default('asdf'); // string
z.string().default('asdf').optional(); // string | undefined
z.string().default('asdf').optional().default('asdf'); // string

5. Introduce .nullish()

// equivalent
z.string().nullish()
z.string().optional().nullable()

@KATT
Copy link
Contributor

KATT commented May 2, 2021

  1. Default values are now implemented in separate ZodDefault class.

No opinion

  1. Standardized behavior of .optional() and .nullable() methods.

No opinion

  1. No undefined defaults

Seems good as long as you don't have to pass a default() to optional() (but seems like you don't considering 4.)

Are you doing a ?? defaultValue in the code, i.e z.string().optional().nullable().default('x').parse(null) return x? I think it should if not.

Can this also .default() used on complex objects too?

  1. Calls to .optional always return schema with T | undefined

Nice.

  1. Introduce .nullish()

You know I love this (#403)


Overall, great changes 👏

Copy link
Sponsor Contributor

@alii alii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fantastic, can't wait to use .nullish! 🚀

@colinhacks
Copy link
Owner Author

Glad you like it :) I was planning to just implement .nullish (basically a one-liner) then I got sucked down this rabbit hole :) Feeling good about it!

Currently defaults are only applied when the schema receives an undefined value. I'd be opening to adding a new subclass (NullishDefault?) or add flag inside ZodDefault to support ??-like behavior.

Regarding the base-class convenience method, this could be introduced as a new method (nullishDefault()?) or as a param on default (.default('asdf', {nullish:true})). Probably gonna do the new method though because I don't like params in general.

@ejose19
Copy link
Contributor

ejose19 commented Jul 15, 2021

@colinhacks If a new method will be added (nullishDefault), then it will be great if a null-only default is provided as well, just to be consistent, and also since T.nullable().default() output type is T | null instead of just T (related #425)

@iambryanhaney
Copy link

@colinhacks +1 for a nullishDefault.

I'm running into some impedance mismatches between Zod and React Hook Forms with regards to undefined / null handling.

RHF discourages the use of undefined for field states and reserves its use internally - explicitly setting a field to undefined will reset its state back to the form default.

Setting the field as null is the 'proper' way, but that means changing the schema from optional() to nullish(), effectively obviating .default() and causing consumers of the form to receive number | null type.

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

Successfully merging this pull request may close these issues.

5 participants