Chaining refinements which narrow down types gives them inconsistent runtime/static types
See original GitHub issueValidation continues after an unsuccessful refine(), which gives it an incorrect return type and can make subsequent effects unexpectedly throw.
Code:
import { z } from 'zod';
const isNotNull = <T>(val: T | null): val is T => val !== null;
const userSchema = z
.object({
type: z.literal('Staff'),
age: z.number(),
})
.nullable()
// Type guard, I would expect validation to halt here if value is null
.refine(isNotNull, { message: 'Choose a user type.', path: ['type'] })
// User is infered as non-null
.superRefine((user, ctx) => {
// This should always return false (even according to TS), but it doesn't
console.log(`superRefine: Is user null: ${user === null}`);
// If user is null, we get an exception here
if (user.age < 18) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
});
}
});
const goodResult = userSchema.safeParse({
type: 'Staff',
age: 20,
});
console.log({
success: goodResult.success,
output: goodResult.success ? goodResult.data : goodResult.error.issues,
});
const issueResult = userSchema.safeParse({
type: 'Staff',
age: 15,
});
console.log({
success: issueResult.success,
output: issueResult.success ? issueResult.data : issueResult.error.issues,
});
const bugResult = userSchema.safeParse(null); // Exception
console.log({
success: bugResult.success,
output: bugResult.success ? bugResult.data : bugResult.error.issues,
});
Output:
superRefine: Is user null: false
{ success: true, output: { type: 'Staff', age: 20 } }
superRefine: Is user null: false
{
success: false,
output: [ { code: 'custom', path: [], message: 'Invalid input' } ]
}
superRefine: Is user null: true
[TypeError: Cannot read properties of null (reading 'age')]
Expected output (last line):
{
success: false,
output: [ { code: 'custom', message: 'Choose a user type.', path: [Array] } ]
}
Yes, there is an easy workaround by adding an if (user !== null) return;
(which I have used for now). But the exception comes out of nowhere and the runtime type is different from the static type (TS tells us user can’t be null) which can be very confusing and make the issue hard to find.
Issue Analytics
- State:
- Created 10 months ago
- Comments:5 (2 by maintainers)
Top Results From Across the Web
Type-Safe TypeScript with Type Narrowing - Rainer Hahnekamp
This article shows common patterns to maximize TypeScript's potential for type-safe code. These techniques are all part of the same group, ...
Read more >Course Script - INF 5110: Compiler con - UiO
This is the script version of the slides shown in the lecture. It contains basically all the slides in the order presented (except...
Read more >Type Narrowing in TypeScript - Medium
There are two key forms of narrowing: refinement and assertion. The former modifies the type using filtering and replacement actions; the latter ...
Read more >Tool Support for Finding and Preventing Faults in Rule Bases
The starting point for this thesis was the apparent contradiction between the perception of rule bases as simple to create and the experience...
Read more >Semantic decomposition and marker passing in an artificial ...
work, we try to make software agents more intelligent by giving them the ability to plan with a semantic and pragmatic understanding of...
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
Alright, I did some cleaning up and split this issue into three: #1602 - Add a way for .superRefine() to narrow down the output type #1603 - Add an option to .refine() to abort early This issue - Discussion/possible ways of fixing the inconsistent typing when chaining refinements.
To me, the most apparent fixes to this issue are:
1 seems easier but leads to incorrect types. I do not know how difficult 2 would be or if it is even doable.
Would love to hear other opinions on this.
It seems to me adding an option to abort on
refine
failure is the way to go in this case, but the docs should mention the possible runtime/static type mismatch if you do not abort (which always happens now).I do also think
superRefine
should have a way of narrowing down types. I could create a separate issue for that.