zod icon indicating copy to clipboard operation
zod copied to clipboard

safeParse discriminated union doesn't have 'error' attribute

Open BrennerSpear opened this issue 2 years ago • 5 comments

const parsedUser = userZ.safeParse(user.toObject())

if (!parsedUser.success) {
      console.error(parsedUser.error)
      return null
}

Gives me this error:

Property 'error' does not exist on type 'SafeParseReturnType<{ telegramId?: number; telegramHandle?: string; twitterId?: number; twitterHandle?: string; address?: string; ens?: string; telegramFirstName?: string; twitterUsername?: string; isMetabotEnabled?: boolean; }, { ...; }>'.
  Property 'error' does not exist on type 'SafeParseSuccess<{ telegramId?: number; telegramHandle?: string; twitterId?: number; twitterHandle?: string; address?: string; ens?: string; telegramFirstName?: string; twitterUsername?: string; isMetabotEnabled?: boolean; }>'.ts(2339)

when success===true it doesn't give an error when i try to access data. Example:

return parsedUser.success ? parsedUser.data : null

BrennerSpear avatar Jun 05 '22 15:06 BrennerSpear

Do you have strict: true in your tsconfig.json? I cannot reproduce the issue in the TypeScript playground:

TypeScript Playground

Screen Shot 2022-06-06 at 11 17 24 AM

scotttrinh avatar Jun 06 '22 15:06 scotttrinh

If strict:false in tsconfig.json then will this fail?

altairmn avatar Jun 07 '22 05:06 altairmn

I am having the same errors trying to access address.data on the last line

        const address = addressSchema.safeParse(req.body);

        if (!address.success) {
          res.status(400).end({ error: address.error });
          resolve();
        }
        const parcedAddress: addressPayload = address.data;

Property 'data' does not exist on type 'SafeParseError Property 'data' does not exist on type 'SafeParseReturnType

nazarEnzo avatar Jun 21 '22 22:06 nazarEnzo

I found a solution:

This fails

 const result = validator.safeParse(example);

if (!result.success) {
    result.error // this shows a TS error
}

This works

 const result = validator.safeParse(example);

if (result.success === false) {
    result.error // it works!
}

Perhaps the docs should update the example so users can infer the correct types.

Daniel-Griffiths avatar Jun 30 '22 19:06 Daniel-Griffiths

Hi, @Daniel-Griffiths! I am using zod v3.14.5 and have no issues using the following:

const AddressSchema = z
    .object({
        id: z.string(),
        street: z.string(),
        number: z.string()
})

// parse & validate payload
const address = AddressScheme.safeParse(req.body);
if (!address.success) return res.status(400).send(address.error.issues);
const parsedAddress = address.data;

nazarEnzo avatar Jul 01 '22 07:07 nazarEnzo

I think the return values of the safeParse and its types needs to be make more consistent. I think both cases must have the same fields, just with different values, either them being the value or null.

type safeParseReturn<T> = { success: true, error: null, data: T } | { sucess: false, error: Error, data: null }

I don't know if there is any specific ts reason to not do it like this

danielo515 avatar Aug 15 '22 08:08 danielo515

I'm also having a very similar issue to this & causing my build to fail.

ERROR: src/pages/contact.tsx:24:15 - error TS2339: Property 'data' does not exist on type 'SafeParseReturnType<{ message?: string; name?: string; email?: string; }, { message?: string; name?: string; email?: string; }>'.

CODE: const { data, error } = ContactForm.safeParse({ name, email, message });

I took the code from the Zod documentation as well but it seems that the SafeParseReturnType doesn't contain either a 'data' or 'error' key that I can look into

ytsruh avatar Aug 20 '22 20:08 ytsruh

@ytsruh

SafeParseReturnType is a discriminated union, so you have to narrow the type before accessing the properties of the result object. A few of the examples above show how it's done, but here's your same example:

const result = ContactForm.safeParse({ name, email, message });

if (!result.success) {
  const { error } = result;
  // use `error`
}

const { data } = result;
// use `data`

scotttrinh avatar Aug 22 '22 19:08 scotttrinh

I think the return values of the safeParse and its types needs to be make more consistent. I think both cases must have the same fields, just with different values, either them being the value or null.

type safeParseReturn<T> = { success: true, error: null, data: T } | { sucess: false, error: Error, data: null }

I don't know if there is any specific ts reason to not do it like this

One could simply use the error and data nullability and not need the success key at all since error and data would serve as their own discriminants. Definitely a design choice, but I think it's consistent with common TypeScript usage of discriminated unions where the keyset of the object is determined by the discriminant, not just the types of the values.

scotttrinh avatar Aug 22 '22 19:08 scotttrinh

Since no one has demonstrated any bug or incorrect results here, I'm going to close this issue so that it doesn't become a dumping ground for similar issues or related discussions.

scotttrinh avatar Aug 22 '22 19:08 scotttrinh

Here is a demo of the issue.

Note foo() fails when narrowing using if (!result.success) { to narrow while bar() passes using if (result.success === false) {

https://stackblitz.com/edit/angular-ivy-hytjux?file=src/app/app.component.ts

dexster avatar Sep 11 '22 15:09 dexster

Found the issue.

In order to do this, if (!result.success), strictNullChecks must be set to true.

Alternatively, you can do this: if (result.success === false)

or

if (!result.success) {
  const { error } = result as SafeParseError;

dexster avatar Sep 12 '22 08:09 dexster

Found the issue.

In order to do this, if (!result.success), strictNullChecks must be set to true.

Alternatively, you can do this: if (result.success === false)

or

if (!result.success) {
  const { error } = result as SafeParseError;

I have strict: true, but I'm still seeing errors when doing !result.success. I'm still having to do a manual comparison with ===.

aaronmcadam avatar Sep 14 '22 17:09 aaronmcadam

I am having the same errors trying to access address.data on the last line

        const address = addressSchema.safeParse(req.body);

        if (!address.success) {
          res.status(400).end({ error: address.error });
          resolve();
        }
        const parcedAddress: addressPayload = address.data;

Property 'data' does not exist on type 'SafeParseError Property 'data' does not exist on type 'SafeParseReturnType I get this error I wonder why, it works on other files though

7566loweayadna11904 avatar Jan 11 '23 17:01 7566loweayadna11904

You need to return the resolve call. Like return resolve()

On Wed, 11 Jan 2023 at 18:51, Lluie Cain Andaya @.***> wrote:

I am having the same errors trying to access address.data on the last line

    const address = addressSchema.safeParse(req.body);

    if (!address.success) {
      res.status(400).end({ error: address.error });
      resolve();
    }
    const parcedAddress: addressPayload = address.data;

Property 'data' does not exist on type 'SafeParseError Property 'data' does not exist on type 'SafeParseReturnType I get this error I wonder why, it works on other files though

— Reply to this email directly, view it on GitHub https://github.com/colinhacks/zod/issues/1190#issuecomment-1379266600, or unsubscribe https://github.com/notifications/unsubscribe-auth/AARKJWOE6HG46ETT2JABQHLWR3XLHANCNFSM5X5GLDNQ . You are receiving this because you commented.Message ID: @.***>

--

https://danielorodriguez.com

danielo515 avatar Jan 12 '23 05:01 danielo515

I think the return values of the safeParse and its types needs to be make more consistent. I think both cases must have the same fields, just with different values, either them being the value or null.

type safeParseReturn<T> = { success: true, error: null, data: T } | { sucess: false, error: Error, data: null }

I don't know if there is any specific ts reason to not do it like this

I pretty much think this is a good idea, library apis that always return same shape results are much easier to work with and reason about.

So I have made a wrapper for safeParse providing a predictable API to work with:

import { z, Schema } from "zod";

interface ParseResult {
    data: object | null;
    error: object | null;
}

function safeParse(schema: Schema, inputData: object):ParseResult {
    var data = null, error = null;
    try {
        const result =  schema.parse(inputData);
        data = result;
    } catch (e) {
        error = e
    }

    return { data, error }
}


// Now you can easily have flows like

const ProjectSchema = z.object({
    title: z
        .string(),
    description: z.string(),
});

const testData = {title: 'foo'  , description: 'bar'};

const {data, error} = safeParse( ProjectSchema, testData);

Maybe some on find it useful too.

daguitosama avatar Feb 12 '23 22:02 daguitosama

Since no one has demonstrated any bug or incorrect results here, I'm going to close this issue so that it doesn't become a dumping ground for similar issues or related discussions.

How is https://stackblitz.com/edit/typescript-baclo6?file=index.ts not a problem? To be fair I don't know whether it's typescript or zod issue, it exists, doesn't it?

shdpl avatar Feb 17 '23 16:02 shdpl

I've noticed that this issue as well:

const foo = z.string().safeParse('hi')
if (!foo.success) throw foo.error // Property 'error' does not exist on type 'SafeParseSuccess<string>'.
console.log(foo.data)

image

The resp.success === false workaround works but feels awkward/buggy behavior as I've been able to use !resp.success previously (not sure if this is a newly introduced issue?)

Using Zod 3.21.4 and TypeScript 4.9.5 (both latest as of writing)

danawoodman avatar Mar 10 '23 21:03 danawoodman

I think the return values of the safeParse and its types needs to be make more consistent. I think both cases must have the same fields, just with different values, either them being the value or null.

type safeParseReturn<T> = { success: true, error: null, data: T } | { sucess: false, error: Error, data: null }

I don't know if there is any specific ts reason to not do it like this

I pretty much think this is a good idea, library apis that always return same shape results are much easier to work with and reason about.

So I have made a wrapper for safeParse providing a predictable API to work with:

import { z, Schema } from "zod";

interface ParseResult {
    data: object | null;
    error: object | null;
}

function safeParse(schema: Schema, inputData: object):ParseResult {
    var data = null, error = null;
    try {
        const result =  schema.parse(inputData);
        data = result;
    } catch (e) {
        error = e
    }

    return { data, error }
}


// Now you can easily have flows like

const ProjectSchema = z.object({
    title: z
        .string(),
    description: z.string(),
});

const testData = {title: 'foo'  , description: 'bar'};

const {data, error} = safeParse( ProjectSchema, testData);

Maybe some on find it useful too.

I think safeParse should have the same results as your implementation. I made some changes in your function to keep the type inference.

import z, { Schema } from 'zod';

interface ParseResult<Shape> {                                                                                                              
  data?: Shape;                                                                                                                             
  error?: z.ZodError<Shape>;                                                                                                                
}                                                                                                                                           
                                                                                                                                            
const safeParse = <Shape>(                                                                                                                  
  schema: Schema<Shape>,                                                                                                                    
  inputData: Partial<Shape>                                                                                                                 
): ParseResult<Shape> => {                                                                                                                  
  const result: ParseResult<Shape> = {};                                                                                                        
  const parsedData = schema.safeParse(inputData);                                                                                              
                                                                                                                                                
  if (parsedData.success === false) {                                                                                                          
    result.error = parsedData.error;                                                                                                              
  } else {                                                                                                                                            
    result.data = parsedData.data;                                                                                                                             
  }                                                                                                                                                   
                                                                                                                                                                
  return result;                                                                                                                                   
};

describe('Test', () => {                                                                                                                                                                                         
  it('should validate is required', () => {                                                                                                                                                                      
    const schema = z.object({ total: z.number() });                                                                                                                                                              
    const result = safeParse(schema, {});                                                                                                                         
                                                                                                                                            
    expect(result.data).toBeUndefined();                                                                                                    
    expect(result.error?.errors).toEqual([                                                                                                  
      {                                                                                                                                     
        code: 'invalid_type',                                                                                                               
        expected: 'number',                                                                                                                 
        message: 'Required',                                                                                                                
        path: ['total'],                                                                                                                    
        received: 'undefined'                                                                                                               
      }                                                                                                                                     
    ]);                                                                                                                                     
  });                                                                                                                                       
});
`

YuriSS avatar May 25 '23 16:05 YuriSS

What solved for me was to set "strict": true and "strictNullChecks": true in the tsconfig.json under the compilerOptions.

The code below now works as expected (no TS complaints):

  const result = schema.safeParse(value)

  if (!result.success) {
    log({ msg: 'Invalid input', error: result.error })
  }

Currently using:

  • "zod": "3.22.2"
  • "typescript": "5.2.2"

hlibco avatar Sep 19 '23 09:09 hlibco

strict and strictNullChecks doesn't really solve the issue that we can't do early returns by just checking if (result.error). it's almost purely a stylistic difference, but it's definitely unexpected and not at all "convenient" like the docs claim

if anything it should get a more honest call out and say that because it's a discriminated union, the usual early return syntax is reversed from what you normally see: you must check for a false success, and not the existence of an error

worc avatar Sep 22 '23 19:09 worc

@scotttrinh

This does not seem like a closed issue and thus should be re-opened.

OffensiveBias-08-145 avatar Dec 19 '23 23:12 OffensiveBias-08-145

This is still an issue

=== workaround works but it is easy to forget about. My editor is perfectly fine with if (result.success) only to then fail on tsc run

gfx687 avatar Jan 30 '24 08:01 gfx687

Please open up, this issue is still active.

mjasnikovs avatar Jan 30 '24 09:01 mjasnikovs

solution `const validatedFields = RegisterSchema.safeParse(values);

if (!validatedFields.success) {
    return { error: "Invalid fields!" };
}
const { email, password, name } = validatedFields.data;`

TA9IO avatar Feb 22 '24 14:02 TA9IO

Easy, go to tsConfig.json and add "strict": true in compilerOptions.

ajmir-github avatar Mar 03 '24 13:03 ajmir-github

I'm going to lock this issue: safeParse uses a result-like discriminated union, and that's a design choice we chose explicitly. We are not likely to ever reverse course on the design of this API.

I'll take a quick stab at a few of the recent messages here:


=== workaround works but it is easy to forget about. My editor is perfectly fine with if (result.success) only to then fail on tsc run

If you're having this issue: you do not have strict: true or are turning it or strictNullChecks off at some point in your config. We require this setting, and it's spelled out explicitly in the docs. Furthermore if your editor and tsc are disagreeing that sounds like an issue with your editor's setup (using a different tsconfig.ts than tsc maybe?)


doesn't really solve the issue that we can't do early returns by just checking if (result.error). it's almost purely a stylistic difference, but it's definitely unexpected and not at all "convenient" like the docs claim

That style of checking is common in JS and Go, but is (arguably) less idomatic TypeScript given result-like discriminated unions. If you disagree with this design on stylistic grounds, that's fine, you're free to disagree, but we have specifically chosen this API, so "I don't like it" isn't an argument we're going to find very compelling.

If I can make an argument for why we chose this it would be: In a language like TypeScript with discriminated unions, it seems less clear that a member of that union would have keys from other members of the union but just have them set to a constant null like value. For instance (excuse the contrived example):

type Shape =
  | { type: "Circle"; radius: number }
  | { type: "Rectangle"; height: number; width: number }

vs

type Shape =
  | { type: "Circle"; width: null; height: null; radius: number; }
  | { type: "Rectangle"; width: number; height: number; radius: null; }

There is another more subtle issue here, too:

const schema = z.null();

const result = schema.safeParse(null);

if (result.data) {
  // oops! this doesn't work because it's supposed to be `null`!
}

You might argue that users aren't likely to fall into this due to the if (result.error) being used for early return, but this requires discipline rather than leveraging the type system to avoid weird foot guns like this.


At any rate, this is working as expected. If we want to maybe open a discussion about future changes to the API, feel free, but this isn't an issue or a bug. Thanks for everyone engaging in the discussion and bringing their ideas and use cases forward here, I don't want to come across as harsh or dismissive.

scotttrinh avatar Mar 04 '24 15:03 scotttrinh