Search code examples
typescriptzod

Type error while transforming input via Zod


I'm trying to write a decoder function (decode) that takes in a Zod schema and an unknown input. It should be able to both validate the input and transform it if the Zod codec's input and output types are different.

When I try to do it like this:

import { ZodSchema, z } from 'zod'

const DateFromString = z.string().transform(input => new Date(input))

const Event = z.object({ timestamp: DateFromString })

const decode = <T>(schema: ZodSchema<T>, data: unknown): T => schema.parse(data)

const { timestamp } = decode(Event, { timestamp: '2024-01-01' })

I get the following type error:

The types of '_input.timestamp' are incompatible between these types. Type 'string' is not assignable to type 'Date'.(2345)

Is there a way to fix this type error and make sure that the timestamp variable at the last line is of type Date?

Another way to ask my question is how can I define just one decode function rather than repeating the implementation just for the sake of typing:

import { ZodArray, ZodObject, ZodRawShape, ZodUnion, ZodUnionOptions } from 'zod'

const decodeObject = <T extends ZodRawShape>(schema: ZodObject<T>, data: unknown) =>
  schema.parse(data)

const decodeArray = <T extends ZodRawShape>(schema: ZodArray<ZodObject<T>>, data: unknown) =>
  schema.parse(data)

const decodeUnion = <T extends ZodUnionOptions>(schema: ZodUnion<T>, data: unknown) =>
  schema.parse(data)

Solution

  • The decode function is wrongly inferring the type as shown in the docs. This version will correctly infer ZodSchema, ZodPipeline and ZodEffects

    function decode<T extends z.ZodTypeAny>(schema: T, data: unknown) {
        return schema.parse(data) as z.infer<T>;
    }
    

    You probably want to validate the Date type, for this use a zod.custom:

    // With 'custom' we need to provide our own validation
    const DateType = z.custom<Date>(
        (input) => new Date(input).toString() !== 'Invalid Date'
    ); // Weak validation
    
    const DateFromString = z
        .string() // validate string
        .transform((v) => new Date(v)) // transform to Date
        .pipe(DateType); // validate Date
    

    Careful, just calling new Date() is not a proper way to validate dates. e.g: new Date(0) is 'valid'; new Date(12345) is also 'valid'.

    Also, Zod has a validation for string in timestamp format:

    const DateFromString = z.string().date();