-
Just wondering how I can infer the type of errors passed into handler, so I can define helper functions that throws same pre-defined errors? export const login = publicProcedure
.input(z.object({ code: z.string() }))
.errors({
BAD_REQUEST: { message: 'Please restart the process', data: z.object({ name: z.string() }) },
})
.handler(async ({ input: { code }, errors }) => {
const something = await doSomething(code, errors)
})
function doSomething(code: string, errors: InferedErrorType) {
// ^^^^^^^^^^^^^^^ How to infer this type?
// ...
} |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 4 replies
-
Hi @s1n7ax! I'm Dosu and I’m helping the orpc team. You can infer the type of pre-defined errors for your helper functions using the utility types from import { InferContractRouterErrorMap, ErrorFromErrorMap } from '@orpc/contract';
// Infer the error map type from your procedure
type LoginErrorMap = InferContractRouterErrorMap<typeof login>;
// Optionally, get the error instance type (if you want to type thrown errors)
type LoginError = ErrorFromErrorMap<LoginErrorMap>;
// Use the inferred type for your helper
function doSomething(
code: string,
errors: LoginErrorMap // or LoginError if you want the instance type
) {
// ...
} This ensures your helpers are always in sync with the errors defined on your procedure, and you get full type safety and autocomplete for error codes and their data schemas. The same pattern works for routers as well as individual procedures. For more details and examples, see the official docs and type utility source. This approach is the recommended and canonical way to infer error types for type-safe helpers in orpc source. To reply, just mention @dosu. How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other |
Beta Was this translation helpful? Give feedback.
-
I'm trying to do something similar for my library where I'm using helper methods which have their own I would like to have a typesafe helper, let's call it Of course, the goal is for the typing to be automatically inferred, so that if in one of my route I forgot to say "this route can return a NOT_FOUND", but that I'm using a helper which can return a NOT_FOUND, the typechecker will complain when I pass export const login = publicProcedure
.input(z.object({ code: z.string() }))
.errors({
BAD_REQUEST: { message: 'Please restart the process', data: z.object({ name: z.string() }) },
})
.handler(async ({ input: { code }, errors }) => {
const myResult = await myHelper(someArgs); // <-- this can be a MyItem or a ApiError<NOT_FOUND>
handleApiResult(result, errors); // <--- THIS SHOULD COMPLAIN, because the definition of `errors` above does not include NOT_FOUND but only BAD_REQUEST
}) for now the hack I found is this (not sure why it works, I'm not versed in typescript type magic haha) import type {
ErrorMap,
ORPCErrorConstructorMap,
// ORPCErrorConstructorMap,
} from "@orpc/server";
import {
type ApiError,
HttpStatus,
type Result,
} from "@workspace/api-schemas/utils";
type HandleErrorsOptions = {
notFoundMessage?: string;
};
// 1. Create some "overload call" types for each possible error
export function handleApiCall<
S,
Map extends { NOT_FOUND: Record<string, unknown> },
>(
result: Result<S, ApiError<HttpStatus.NOT_FOUND>>,
errors: ORPCErrorConstructorMap<Map>,
options?: HandleErrorsOptions,
): S;
export function handleApiCall<
S,
Map extends {
BAD_REQUEST: Record<string, unknown>
},
>(
result: Result<S, ApiError<HttpStatus.BAD_REQUEST>>,
errors: ORPCErrorConstructorMap<Map>,
options?: HandleErrorsOptions,
): S;
// ... <-- add other overloaded call definitions for all the errors
// 2. Create the base implementation (which I don't manage to type fully correctly either, hence all the `(errors as any)` in the code
export function handleApiCall<
S,
T extends
| HttpStatus.NOT_FOUND
| HttpStatus.BAD_REQUEST
| HttpStatus.FORBIDDEN
| HttpStatus.UNPROCESSABLE_CONTENT
| HttpStatus.INTERNAL_SERVER_ERROR
| HttpStatus.UNAUTHORIZED
| HttpStatus.METHOD_NOT_SUPPORTED
| HttpStatus.NOT_ACCEPTABLE
| HttpStatus.REQUEST_TIMEOUT
| HttpStatus.CONFLICT
| HttpStatus.PRECONDITION_FAILED
| HttpStatus.PAYLOAD_TOO_LARGE
| HttpStatus.UNSUPPORTED_MEDIA_TYPE
| HttpStatus.TOO_MANY_REQUESTS
| HttpStatus.CLIENT_CLOSED_REQUEST
| HttpStatus.NOT_IMPLEMENTED
| HttpStatus.BAD_GATEWAY
| HttpStatus.SERVICE_UNAVAILABLE
| HttpStatus.GATEWAY_TIMEOUT,
Map extends ErrorMap,
>(
result: Result<S, ApiError<T>>,
errors: ORPCErrorConstructorMap<Map>,
options?: HandleErrorsOptions,
): S {
if (result.isErr()) {
const error = result.unwrapErr();
switch (error.status) {
case HttpStatus.HttpStatusCodes.NOT_FOUND:
// biome-ignore lint/suspicious/noExplicitAny: for typing
throw (errors as any).NOT_FOUND({
message: options?.notFoundMessage ?? error.message,
});
case HttpStatus.HttpStatusCodes.BAD_REQUEST:
// biome-ignore lint/suspicious/noExplicitAny: for typing
throw (errors as any).BAD_REQUEST({ message: error.message });
case HttpStatus.HttpStatusCodes.UNAUTHORIZED:
// biome-ignore lint/suspicious/noExplicitAny: for typing
throw (errors as any).UNAUTHORIZED({ message: error.message });
case HttpStatus.HttpStatusCodes.FORBIDDEN:
// biome-ignore lint/suspicious/noExplicitAny: for typing
throw (errors as any).FORBIDDEN({ message: error.message });
// ... <- ADD ALL THE REMAINING ERROR HANDLERS
default:
// biome-ignore lint/suspicious/noExplicitAny: for typing
throw (errors as any).INTERNAL_SERVER_ERROR({ message: error.message });
}
}
return result.unwrap();
} |
Beta Was this translation helpful? Give feedback.
-
Following exchange with @unnoq I simplified slightly to this export function handleApiCall<S>(
result: Result<S, ApiError<HttpStatus.NOT_FOUND>>,
errors: { NOT_FOUND: () => ORPCError<"NOT_FOUND", unknown> },
options?: HandleErrorsOptions,
): S;
export function handleApiCall<S>(
result: Result<S, ApiError<HttpStatus.BAD_REQUEST>>,
errors: { BAD_REQUEST: () => ORPCError<"BAD_REQUEST", unknown> },
options?: HandleErrorsOptions,
): S;
// ... ADD OTHER ERRORS
export function handleApiCall<
S,
T extends
| HttpStatus.NOT_FOUND
| HttpStatus.BAD_REQUEST
| HttpStatus.FORBIDDEN
| HttpStatus.UNPROCESSABLE_CONTENT
| HttpStatus.INTERNAL_SERVER_ERROR
| HttpStatus.UNAUTHORIZED
| HttpStatus.METHOD_NOT_SUPPORTED
| HttpStatus.NOT_ACCEPTABLE
| HttpStatus.REQUEST_TIMEOUT
| HttpStatus.CONFLICT
| HttpStatus.PRECONDITION_FAILED
| HttpStatus.PAYLOAD_TOO_LARGE
| HttpStatus.UNSUPPORTED_MEDIA_TYPE
| HttpStatus.TOO_MANY_REQUESTS
| HttpStatus.CLIENT_CLOSED_REQUEST
| HttpStatus.NOT_IMPLEMENTED
| HttpStatus.BAD_GATEWAY
| HttpStatus.SERVICE_UNAVAILABLE
| HttpStatus.GATEWAY_TIMEOUT,
>(
result: Result<S, ApiError<T>>,
errors: Record<string, unknown>,
options?: HandleErrorsOptions,
): S {
if (result.isErr()) {
const error = result.unwrapErr();
switch (error.status) {
case HttpStatus.HttpStatusCodes.NOT_FOUND:
// biome-ignore lint/suspicious/noExplicitAny: for typing
throw (errors as any).NOT_FOUND({
message: options?.notFoundMessage ?? error.message,
});
case HttpStatus.HttpStatusCodes.BAD_REQUEST:
// biome-ignore lint/suspicious/noExplicitAny: for typing
throw (errors as any).BAD_REQUEST({ message: error.message });
// ... ADD OTHER ERRORS
default:
// biome-ignore lint/suspicious/noExplicitAny: for typing
throw (errors as any).INTERNAL_SERVER_ERROR({ message: error.message });
}
}
return result.unwrap();
} |
Beta Was this translation helpful? Give feedback.
-
Ideally what I would really like is to be able to remove all the manual overloads, and have just some kind of: export function handleApiCall<
S,
T extends
| HttpStatus.NOT_FOUND
| HttpStatus.BAD_REQUEST
| HttpStatus.FORBIDDEN
| HttpStatus.UNPROCESSABLE_CONTENT
| HttpStatus.INTERNAL_SERVER_ERROR
| HttpStatus.UNAUTHORIZED
| HttpStatus.METHOD_NOT_SUPPORTED
| HttpStatus.NOT_ACCEPTABLE
| HttpStatus.REQUEST_TIMEOUT
| HttpStatus.CONFLICT
| HttpStatus.PRECONDITION_FAILED
| HttpStatus.PAYLOAD_TOO_LARGE
| HttpStatus.UNSUPPORTED_MEDIA_TYPE
| HttpStatus.TOO_MANY_REQUESTS
| HttpStatus.CLIENT_CLOSED_REQUEST
| HttpStatus.NOT_IMPLEMENTED
| HttpStatus.BAD_GATEWAY
| HttpStatus.SERVICE_UNAVAILABLE
| HttpStatus.GATEWAY_TIMEOUT,
>(
result: Result<S, ApiError<T>>,
errors: // <-- how to type it like " if T includes HttpStatus.NOT_FOUND, then errors MUST INCLUDE { NOT_FOUND: () => ORPCError<"NOT_FOUND", unknown>", etc. for all errors
options?: HandleErrorsOptions,
): S {
if (result.isErr()) {
const error = result.unwrapErr();
switch (error.status) {
case HttpStatus.HttpStatusCodes.NOT_FOUND:
// biome-ignore lint/suspicious/noExplicitAny: for typing
throw errors.NOT_FOUND({ // <--- THIS WOULD BE WELL TYPED, AND DON'T NEED THE "as any" above because it "knows" that errors has NOT_FOUND if the ApiError<T> includes HttpStatus.NOT_FOUND
message: options?.notFoundMessage ?? error.message,
});
... |
Beta Was this translation helpful? Give feedback.
Awesome. This lib almost like magic. Minor correction I thing. Looks like there is no direct way to get the callable map. Type of
LoginErrorMap
isBut Wrapping it around
ORPCErrorConstructorMap
worked