Skip to content

Commit

Permalink
feat: narrow superRefine() type (colinhacks#1615)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxArturo authored Dec 12, 2022
1 parent 9df6be9 commit 497d44b
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 4 deletions.
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1799,7 +1799,6 @@ z.string()

<!-- Note that the `path` is set to `["confirm"]` , so you can easily display this error underneath the "Confirm password" textbox.
```ts
const allForms = z.object({ passwordForm }).parse({
passwordForm: {
Expand Down Expand Up @@ -1876,6 +1875,35 @@ const schema = z.number().superRefine((val, ctx) => {
});
```

#### Type refinements

If you provide a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) to `.refine()` or `superRefine()`, the resulting type will be narrowed down to your predicate's type. This is useful if you are mixing multiple chained refinements and transformations:

```ts
const schema = z
.object({
first: z.string(),
second: z.number(),
})
.nullable()
.superRefine((arg, ctx): arg is {first: string, second: number} => {
if (!arg) {
ctx.addIssue({
code: z.ZodIssueCode.custom, // customize your issue
message: "object should exist",
});
return false;
}
return true;
})
// here, TS knows that arg is not null
.refine((arg) => arg.first === "bob", "`first` is not `bob`!");


```

> ⚠️ You must **still** call `ctx.addIssue()` if using `superRefine()` with a type predicate function. Otherwise the refinement won't be validated.
### `.transform`

To transform data after parsing, use the `transform` method.
Expand Down
30 changes: 29 additions & 1 deletion deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1799,7 +1799,6 @@ z.string()

<!-- Note that the `path` is set to `["confirm"]` , so you can easily display this error underneath the "Confirm password" textbox.
```ts
const allForms = z.object({ passwordForm }).parse({
passwordForm: {
Expand Down Expand Up @@ -1876,6 +1875,35 @@ const schema = z.number().superRefine((val, ctx) => {
});
```

#### Type refinements

If you provide a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) to `.refine()` or `superRefine()`, the resulting type will be narrowed down to your predicate's type. This is useful if you are mixing multiple chained refinements and transformations:

```ts
const schema = z
.object({
first: z.string(),
second: z.number(),
})
.nullable()
.superRefine((arg, ctx): arg is {first: string, second: number} => {
if (!arg) {
ctx.addIssue({
code: z.ZodIssueCode.custom, // customize your issue
message: "object should exist",
});
return false;
}
return true;
})
// here, TS knows that arg is not null
.refine((arg) => arg.first === "bob", "`first` is not `bob`!");


```

> ⚠️ You must **still** call `ctx.addIssue()` if using `superRefine()` with a type predicate function. Otherwise the refinement won't be validated.
### `.transform`

To transform data after parsing, use the `transform` method.
Expand Down
58 changes: 58 additions & 0 deletions deno/lib/__tests__/refine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,64 @@ test("superRefine", () => {
Strings.parse(["asfd", "qwer"]);
});

test("superRefine - type narrowing", () => {
type NarrowType = { type: string; age: number };
const schema = z
.object({
type: z.string(),
age: z.number(),
})
.nullable()
.superRefine((arg, ctx): arg is NarrowType => {
if (!arg) {
// still need to make a call to ctx.addIssue
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "cannot be null",
fatal: true,
});
return false;
}
return true;
});

util.assertEqual<z.infer<typeof schema>, NarrowType>(true);

expect(schema.safeParse({ type: "test", age: 0 }).success).toEqual(true);
expect(schema.safeParse(null).success).toEqual(false);
});

test("chained mixed refining types", () => {
type firstRefinement = { first: string; second: number; third: true };
type secondRefinement = { first: "bob"; second: number; third: true };
type thirdRefinement = { first: "bob"; second: 33; third: true };
const schema = z
.object({
first: z.string(),
second: z.number(),
third: z.boolean(),
})
.nullable()
.refine((arg): arg is firstRefinement => !!arg?.third)
.superRefine((arg, ctx): arg is secondRefinement => {
util.assertEqual<typeof arg, firstRefinement>(true);
if (arg.first !== "bob") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "`first` property must be `bob`",
});
return false;
}
return true;
})
.refine((arg): arg is thirdRefinement => {
util.assertEqual<typeof arg, secondRefinement>(true);
return arg.second === 33;
});

util.assertEqual<z.infer<typeof schema>, thirdRefinement>(true);
});

test("get inner type", () => {
z.string()
.refine(() => true)
Expand Down
13 changes: 12 additions & 1 deletion deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,18 @@ export abstract class ZodType<
effect: { type: "refinement", refinement },
});
}
superRefine = this._refinement;

superRefine<RefinedOutput extends Output>(
refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput
): ZodEffects<this, RefinedOutput, Input>;
superRefine(
refinement: (arg: Output, ctx: RefinementCtx) => void
): ZodEffects<this, Output, Input>;
superRefine(
refinement: (arg: Output, ctx: RefinementCtx) => unknown
): ZodEffects<this, Output, Input> {
return this._refinement(refinement);
}

constructor(def: Def) {
this._def = def;
Expand Down
58 changes: 58 additions & 0 deletions src/__tests__/refine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,64 @@ test("superRefine", () => {
Strings.parse(["asfd", "qwer"]);
});

test("superRefine - type narrowing", () => {
type NarrowType = { type: string; age: number };
const schema = z
.object({
type: z.string(),
age: z.number(),
})
.nullable()
.superRefine((arg, ctx): arg is NarrowType => {
if (!arg) {
// still need to make a call to ctx.addIssue
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "cannot be null",
fatal: true,
});
return false;
}
return true;
});

util.assertEqual<z.infer<typeof schema>, NarrowType>(true);

expect(schema.safeParse({ type: "test", age: 0 }).success).toEqual(true);
expect(schema.safeParse(null).success).toEqual(false);
});

test("chained mixed refining types", () => {
type firstRefinement = { first: string; second: number; third: true };
type secondRefinement = { first: "bob"; second: number; third: true };
type thirdRefinement = { first: "bob"; second: 33; third: true };
const schema = z
.object({
first: z.string(),
second: z.number(),
third: z.boolean(),
})
.nullable()
.refine((arg): arg is firstRefinement => !!arg?.third)
.superRefine((arg, ctx): arg is secondRefinement => {
util.assertEqual<typeof arg, firstRefinement>(true);
if (arg.first !== "bob") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "`first` property must be `bob`",
});
return false;
}
return true;
})
.refine((arg): arg is thirdRefinement => {
util.assertEqual<typeof arg, secondRefinement>(true);
return arg.second === 33;
});

util.assertEqual<z.infer<typeof schema>, thirdRefinement>(true);
});

test("get inner type", () => {
z.string()
.refine(() => true)
Expand Down
13 changes: 12 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,18 @@ export abstract class ZodType<
effect: { type: "refinement", refinement },
});
}
superRefine = this._refinement;

superRefine<RefinedOutput extends Output>(
refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput
): ZodEffects<this, RefinedOutput, Input>;
superRefine(
refinement: (arg: Output, ctx: RefinementCtx) => void
): ZodEffects<this, Output, Input>;
superRefine(
refinement: (arg: Output, ctx: RefinementCtx) => unknown
): ZodEffects<this, Output, Input> {
return this._refinement(refinement);
}

constructor(def: Def) {
this._def = def;
Expand Down

0 comments on commit 497d44b

Please sign in to comment.