colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.96k stars 1.19k forks source link

Overloaded function return schema with z.union #3592

Open MikeDabrowski opened 4 months ago

MikeDabrowski commented 4 months ago

We are using zod with ts-rest and node express.

I created and endpoint that uses a function which can return 3 different types based on one argument. The function is getFile and the argument is serveAs: 'stream' | 'url' | 'data'. The way I coded it is function overload statements.

export async function getFile(serveAs: 'data'): Promise<File>;
export async function getFile(serveAs: 'stream'): Promise<{ file: File, fileStream: SdkStream<IncomingMessage> }>;
export async function getFile(serveAs: 'url'): Promise<{ url: string }>;
export async function getFile(serveAs: 'data' | 'file' | 'url' = 'file' ) { ... }

Because we are using sequelize v6, the model is still "in js code" and lacks proper and robust typing. The model is simply the File returned from the topmost overload, and at that point is a JSON object (calling toJSON after getting the response from the db).

The contract for this endpoint is declared as a z.union

z.union([
  // For serveAs: url
  z.object({ url: z.string() }),
  // For serveAs: file
  z.instanceof(Stream),
  // For serveAs: data
  FileDto
])

where FileDto is the schema reflecting the db model z.object({ ... }).

Localy it runs fine. No problems whatsoever. But once I pushed the code for a PR our github action for testing failed complaining.

Error: Jest: Got error running globalSetup - /codebuild ... testSetup.js, reason: [TSError: app/controllers/emi/files.controller.ts:32:14 - error TS2322: Type '({ params, res, query }: { params: { id: string; }; query: { serveAs?: "data" | "file" | "presigned-url" | undefined; expiresIn?: number | undefined; }; headers: { [x: string]: string | string[] | undefined; [x: number]: string | ... 1 more ... | undefined; authorization?: string | undefined; "x-product-name"?: stri...' is not assignable to type 'AppRouteQueryImplementation<{ metadata: { 'x-audience': Audience; tags: string[]; operationId: string; }; method: "GET"; description: "Get file by id"; query: ZodObject<{ serveAs: ZodOptional<ZodEnum<["file", "presigned-url", "data"]>>; expiresIn: ZodOptional<...>; }, "strip", ZodTypeAny, { ...; }, { ...; }>; ... 4 ...'.
  Type 'Promise<{ status: 200; body: Stream; } | { status: 200; body: string; } | { status: 200; body: File; }>' is not assignable to type 'Promise<Prettify<AppRouteResponses<{ metadata: { 'x-audience': Audience; tags: string[]; operationId: string; }; method: "GET"; description: "Get file by id"; query: ZodObject<{ serveAs: ZodOptional<ZodEnum<["file", "presigned-url", "data"]>>; expiresIn: ZodOptional<...>; }, "strip", ZodTypeAny, { ...; }, { ...; }>;...'.
    Type '{ status: 200; body: Stream; } | { status: 200; body: { url: string }; } | { status: 200; body: File; }' is not assignable to type 'Prettify<AppRouteResponses<{ metadata: { 'x-audience': Audience; tags: string[]; operationId: string; }; method: "GET"; description: "Get file by id"; query: ZodObject<{ serveAs: ZodOptional<ZodEnum<["file", "presigned-url", "data"]>>; expiresIn: ZodOptional<...>; }, "strip", ZodTypeAny, { ...; }, { ...; }>; ... 4 m...'.
      Type '{ status: 200; body: File; }' is not assignable to type 'Prettify<AppRouteResponses<{ metadata: { 'x-audience': Audience; tags: string[]; operationId: string; }; method: "GET"; description: "Get file by id"; query: ZodObject<{ serveAs: ZodOptional<ZodEnum<["file", "presigned-url", "data"]>>; expiresIn: ZodOptional<...>; }, "strip", ZodTypeAny, { ...; }, { ...; }>; ... 4 m...'.
        Type '{ status: 200; body: File; }' is not assignable to type '{ status: 200; body: string | Stream | { key: string; location: string; id: string; metadata: { key: string; value: string; }[]; path: string; size: number; createdAt: Date; updatedAt: Date; ... 5 more ...; cdnLing?: string | undefined; }; }'.
          Types of property 'body' are incompatible.
            Type 'File' is not assignable to type '{ url: string } | Stream | { key: string; location: string; id: string; metadata: { key: string; value: string; }[]; path: string; size: number; createdAt: Date; updatedAt: Date; bucket: string; ... 4 more ...; cdnLing?: string | undefined; }'.
              Type 'File' is missing the following properties from type '{ key: string; location: string; id: string; metadata: { key: string; value: string; }[]; path: string; size: number; createdAt: Date; updatedAt: Date; bucket: string; mimeType: string; originalName: string; extension: string; version?: string | undefined; cdnLing?: string | undefined; }': metadata, extension

32 export const findFileById: AppRouteImplementation<
                ~~~~~~~~~~~~]
    at runGlobalHook

If I were to give up, I would just split the endpoint into three separate endpoints for each type. Just not sure if the problem wont be the same in the one that returns FileDto/File

Can anyone tell me what am I doing wrong here?

PS: I can't fix the typo cdnLing - it does not appear in code anymore but pops up in the log on github...