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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1434,7 +1434,7 @@ numberWithRandomDefault.parse(undefined); // => 0.7223408162401552

### `.optional`

A convenience method that returns an optional version of a schema.
A convenience method that returns an optional version of the schema.

```ts
const optionalString = z.string().optional(); // string | undefined
Expand All @@ -1445,7 +1445,7 @@ z.optional(z.string());

### `.nullable`

A convenience method that returns an nullable version of a schema.
A convenience method that returns a nullable version of the schema.

```ts
const nullableString = z.string().nullable(); // string | null
Expand All @@ -1454,6 +1454,18 @@ const nullableString = z.string().nullable(); // string | null
z.nullable(z.string());
```

### `.nullish`

A convenience method that returns a nullish version of the schema. Nullish means the schema will allow both `undefined` and `null` values.

```ts
const nullishString = z.string().nullish(); // string | null | undefined

// equivalent to
z.string().optional().nullable();
z.nullable(z.optional(z.string()));
```

### `.array`

A convenience method that returns an array schema for the given type:
Expand Down
2 changes: 1 addition & 1 deletion coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 12 additions & 7 deletions deno/lib/__tests__/default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test("default with transform", () => {
.transform((val) => val.toUpperCase())
.default("default");
expect(stringWithDefault.parse(undefined)).toBe("DEFAULT");
expect(stringWithDefault).toBeInstanceOf(z.ZodOptional);
expect(stringWithDefault).toBeInstanceOf(z.ZodDefault);
expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodEffects);
expect(stringWithDefault._def.innerType._def.schema).toBeInstanceOf(
z.ZodSchema
Expand All @@ -32,15 +32,16 @@ test("default with transform", () => {
test("default on existing optional", () => {
const stringWithDefault = z.string().optional().default("asdf");
expect(stringWithDefault.parse(undefined)).toBe("asdf");
expect(stringWithDefault).toBeInstanceOf(z.ZodOptional);
expect(stringWithDefault).toBeInstanceOf(z.ZodDefault);
expect(stringWithDefault._def.innerType).toBeInstanceOf(z.ZodOptional);
expect(stringWithDefault._def.innerType._def.innerType).toBeInstanceOf(
z.ZodString
);

type inp = z.input<typeof stringWithDefault>;
const f1: util.AssertEqual<inp, string | undefined> = true;
type out = z.output<typeof stringWithDefault>;
const f2: util.AssertEqual<out, string | undefined> = true;
const f2: util.AssertEqual<out, string> = true;
f1;
f2;
});
Expand All @@ -51,7 +52,7 @@ test("optional on default", () => {
type inp = z.input<typeof stringWithDefault>;
const f1: util.AssertEqual<inp, string | undefined> = true;
type out = z.output<typeof stringWithDefault>;
const f2: util.AssertEqual<out, string> = true;
const f2: util.AssertEqual<out, string | undefined> = true;
f1;
f2;
});
Expand All @@ -60,7 +61,6 @@ test("complex chain example", () => {
const complex = z
.string()
.default("asdf")
.optional()
.transform((val) => val.toUpperCase())
.default("qwer")
.removeDefault()
Expand All @@ -74,9 +74,8 @@ test("removeDefault", () => {
const stringWithRemovedDefault = z.string().default("asdf").removeDefault();

type out = z.output<typeof stringWithRemovedDefault>;
const f2: util.AssertEqual<out, string | undefined> = true;
const f2: util.AssertEqual<out, string> = true;
f2;
expect(stringWithRemovedDefault.parse(undefined)).toBe(undefined);
});

test("nested", () => {
Expand All @@ -97,3 +96,9 @@ test("nested", () => {
expect(outer.parse({})).toEqual({ inner: "asdf" });
expect(outer.parse({ inner: undefined })).toEqual({ inner: "asdf" });
});

test("chained defaults", () => {
const stringWithDefault = z.string().default("inner").default("outer");
const result = stringWithDefault.parse(undefined);
expect(result).toEqual("outer");
});
4 changes: 2 additions & 2 deletions deno/lib/__tests__/partials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ test("required", () => {
const object = z.object({
name: z.string(),
age: z.number().optional(),
field: z.string().optional().default(undefined),
field: z.string().optional().default("asdf"),
});

const requiredObject = object.required();
expect(requiredObject.shape.name).toBeInstanceOf(z.ZodString);
expect(requiredObject.shape.age).toBeInstanceOf(z.ZodNumber);
expect(requiredObject.shape.field).toBeInstanceOf(z.ZodString);
expect(requiredObject.shape.field).toBeInstanceOf(z.ZodDefault);
});
124 changes: 67 additions & 57 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,13 @@ export abstract class ZodType<
this.default = this.default.bind(this);
}

optional: <This extends this = this>() => ZodOptionalType<This> = () =>
optional: <This extends this = this>() => ZodOptional<This> = () =>
ZodOptional.create(this) as any;
nullable: <This extends this = this>() => ZodNullableType<This> = () =>
nullable: <This extends this = this>() => ZodNullable<This> = () =>
ZodNullable.create(this) as any;
nullish: <This extends this = this>() => ZodNullable<
ZodOptional<This>
> = () => this.optional().nullable();

array: () => ZodArray<this> = () => ZodArray.create(this);

Expand Down Expand Up @@ -359,12 +362,12 @@ export abstract class ZodType<
return returnType;
}

default<T extends Input, This extends this = this>(
def: T
): ZodOptional<This, true>;
default<T extends () => Input, This extends this = this>(
def: T
): ZodOptional<This, true>;
default<This extends this = this>(
def: util.noUndefined<Input>
): ZodDefault<This>;
default<This extends this = this>(
def: () => util.noUndefined<Input>
): ZodDefault<This>;
default(def: any) {
const defaultValueFunc = typeof def === "function" ? def : () => def;
// if (this instanceof ZodOptional) {
Expand All @@ -373,7 +376,7 @@ export abstract class ZodType<
// defaultValue: defaultValueFunc,
// }) as any;
// }
return new ZodOptional({
return new ZodDefault({
innerType: this,
defaultValue: defaultValueFunc,
}) as any;
Expand Down Expand Up @@ -1298,7 +1301,7 @@ export type objectInputType<
baseObjectInputType<Shape> & { [k: string]: Catchall["_input"] }
>;

type deoptional<T extends ZodTypeAny> = T extends ZodOptional<infer U, any>
type deoptional<T extends ZodTypeAny> = T extends ZodOptional<infer U>
? deoptional<U>
: T;

Expand Down Expand Up @@ -1578,7 +1581,7 @@ export class ZodObject<
const fieldSchema = this.shape[key];
let newField = fieldSchema;
while (newField instanceof ZodOptional) {
newField = (newField as ZodOptional<any, any>)._def.innerType;
newField = (newField as ZodOptional<any>)._def.innerType;
}

newShape[key] = newField;
Expand Down Expand Up @@ -2597,40 +2600,19 @@ export { ZodEffects as ZodTransformer };
export interface ZodOptionalDef<T extends ZodTypeAny = ZodTypeAny>
extends ZodTypeDef {
innerType: T;
defaultValue: undefined | (() => T["_input"]);
}

export type addDefaultToOptional<
T extends ZodOptional<any, any>
> = T extends ZodOptional<infer U, any> ? ZodOptional<U, true> : never;

export type removeDefaultFromOptional<
T extends ZodOptional<any, any>
> = T extends ZodOptional<infer U, any> ? ZodOptional<U, false> : never;

export type ZodOptionalType<T extends ZodTypeAny> = T extends ZodOptional<
infer U,
infer H
>
? ZodOptional<U, H>
: ZodOptional<T, false>; // no default by default
export type ZodOptionalType<T extends ZodTypeAny> = ZodOptional<T>;

export class ZodOptional<
T extends ZodTypeAny,
HasDefault extends boolean = false
> extends ZodType<
HasDefault extends true ? T["_output"] : T["_output"] | undefined,
export class ZodOptional<T extends ZodTypeAny> extends ZodType<
T["_output"] | undefined,
ZodOptionalDef<T>,
T["_input"] | undefined
> {
_parse(ctx: ParseContext): any {
let data = ctx.data;
const data = ctx.data;
if (ctx.parsedType === ZodParsedType.undefined) {
if (this._def.defaultValue !== undefined) {
data = this._def.defaultValue();
} else {
return undefined;
}
return undefined;
}

return new PseudoPromise().then(() => {
Expand All @@ -2645,18 +2627,9 @@ export class ZodOptional<
return this._def.innerType;
}

removeDefault(): ZodOptional<T, false> {
return new ZodOptional({
...this._def,
defaultValue: undefined,
});
}

static create = <T extends ZodTypeAny>(type: T): ZodOptionalType<T> => {
if (type instanceof ZodOptional) return type as any;
static create = <T extends ZodTypeAny>(type: T): ZodOptional<T> => {
return new ZodOptional({
innerType: type,
defaultValue: undefined,
}) as any;
};
}
Expand All @@ -2673,12 +2646,7 @@ export interface ZodNullableDef<T extends ZodTypeAny = ZodTypeAny>
innerType: T;
}

// This type allows for nullable flattening
export type ZodNullableType<T extends ZodTypeAny> = T extends ZodNullable<
infer U
>
? ZodNullable<U>
: ZodNullable<T>;
export type ZodNullableType<T extends ZodTypeAny> = ZodNullable<T>;

export class ZodNullable<T extends ZodTypeAny> extends ZodType<
T["_output"] | null,
Expand All @@ -2702,15 +2670,56 @@ export class ZodNullable<T extends ZodTypeAny> extends ZodType<
return this._def.innerType;
}

static create = <T extends ZodTypeAny>(type: T): ZodNullableType<T> => {
// An nullable nullable is the original nullable
if (type instanceof ZodNullable) return type as any;
static create = <T extends ZodTypeAny>(type: T): ZodNullable<T> => {
return new ZodNullable({
innerType: type,
}) as any;
};
}

////////////////////////////////////////////
////////////////////////////////////////////
////////// //////////
////////// ZodDefault //////////
////////// //////////
////////////////////////////////////////////
////////////////////////////////////////////
export interface ZodDefaultDef<T extends ZodTypeAny = ZodTypeAny>
extends ZodTypeDef {
innerType: T;
defaultValue: () => util.noUndefined<T["_input"]>;
}

export class ZodDefault<T extends ZodTypeAny> extends ZodType<
util.noUndefined<T["_output"]>,
ZodDefaultDef<T>,
T["_input"] | undefined
> {
_parse(ctx: ParseContext): any {
let data = ctx.data;
if (ctx.parsedType === ZodParsedType.undefined) {
data = this._def.defaultValue();
}

return new PseudoPromise().then(() => {
return this._def.innerType._parseWithInvalidFallback(data, {
...ctx,
parentError: ctx.currentError,
});
});
}

removeDefault() {
return this._def.innerType;
}

static create = <T extends ZodTypeAny>(type: T): ZodOptional<T> => {
return new ZodOptional({
innerType: type,
}) as any;
};
}

export const custom = <T>(
check?: (data: unknown) => any,
params?: Parameters<ZodTypeAny["refine"]>[1]
Expand Down Expand Up @@ -2751,8 +2760,9 @@ export type ZodFirstPartySchemaTypes =
| ZodEnum<any>
| ZodEffects<any>
| ZodNativeEnum<any>
| ZodOptional<any, any>
| ZodOptional<any>
| ZodNullable<any>
| ZodDefault<any>
| ZodPromise<any>;

const instanceOfType = <T extends new (...args: any[]) => any>(
Expand Down
Loading