fastify-type-provider-zod icon indicating copy to clipboard operation
fastify-type-provider-zod copied to clipboard

[Question] Discriminating response schema by status code

Open ivstiv opened this issue 2 years ago • 4 comments

I am wondering if there is an integrated way to restrict the response type based on the status code being set in the request at compile time? I guess I can write my own function to set the status code and return the response doing the discrimination myself but it does seem like a feature that should be part of the library.

ivstiv avatar Jan 28 '23 21:01 ivstiv

I was wondering this myself. I think this can be accomplished in Fastify TypeScript. This is a declaration from https://github.com/fastify/fastify/blob/main/types/reply.d.ts

export interface FastifyReply<
  RawServer extends RawServerBase = RawServerDefault,
  RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
  RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
  RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
  ContextConfig = ContextConfigDefault,
  SchemaCompiler extends FastifySchema = FastifySchema,
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
  ReplyType extends FastifyReplyType = ResolveFastifyReplyType<TypeProvider, SchemaCompiler, RouteGeneric>
> {
  // ...
  code(statusCode: number): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>;
  // ....
  }

So, this might be possible to discriminate in the return type of .code() narrowing ReplyType down, but I'm not sure that TypeScript allows to do this...

In the meantime, I'm going to use this function

function typesafeReply<ResponseDtos extends Record<number, unknown>>(
  reply: FastifyReply
) {
  return <Code extends number>(code: Code) => {
    return (replyData: ResponseDtos[Code]) => {
      return reply.code(code).send(replyData);
    };
  };
}

// ...

const MyResponseDtos = {
  200: zod.object({ r: zod.string(), n: zod.number() }),
  400: zod.string(),
};

type MyResponseDtos = {
  [P in keyof typeof MyResponseDtos]: zod.infer<
    (typeof MyResponseDtos)[P]
  >;
};

typesafeReply<MyResponseDtos>(reply)(200)('string');
// ^ Error
typesafeReply<MyResponseDtos>(reply)(400)('string');
// ^ Ok

@ivstiv what did you end up doing?

dany-fedorov avatar Mar 09 '23 08:03 dany-fedorov

I wrote a similar function to yours but it felt ugly to be passing types, reply and the actual response data, so I decided to keep it simple and reverted back to return rep.code().send(). Yes it is opening the door for a human error in the status codes but equally my response objects have status code literals in them, so it would be a pretty obvious mistake.

I also tried writing a fastify plugin to decorate the reply (+ declaration merging) with a generic function that infers types from the response schema, but to no avail. May be I need to step up my type game to get it right.

ivstiv avatar Mar 09 '23 10:03 ivstiv

Oh, I feel that this is a nasty typescript problem to solve. Either it works and it is a miracle or you spend hours ultimately getting nothing, hehe. Anyway, thanks for sharing

dany-fedorov avatar Mar 09 '23 11:03 dany-fedorov

Check this out: https://www.youtube.com/watch?v=9N50YV5NHaE

kevbook avatar Sep 03 '23 23:09 kevbook