question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Chaining refinements which narrow down types gives them inconsistent runtime/static types

See original GitHub issue

Validation 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:open
  • Created 10 months ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
DanielBreinercommented, Nov 25, 2022

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. Implement #1603 and add a warning to the docs stating that chaining refinements will cause this type inconsistency.
  2. Give effects another type parameter which has the non-narrowed type (output type stays narrowed).

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.

0reactions
DanielBreinercommented, Nov 25, 2022

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.

Read more comments on GitHub >

github_iconTop 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 >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found