turkerdev / fastify-type-provider-zod

MIT License
411 stars 25 forks source link

[Question] Discriminating response schema by status code #36

Open ivstiv opened 1 year ago

ivstiv commented 1 year ago

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.

dany-fedorov commented 1 year ago

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?

ivstiv commented 1 year ago

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.

dany-fedorov commented 1 year ago

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

kevbook commented 1 year ago

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