UNPKG

@ts-rest/core

Version:

RPC-like experience over a regular REST API, with type safe server implementations 🪄

179 lines (178 loc) • 7.71 kB
import { Merge, Opaque, Prettify, WithoutUnknown } from './type-utils'; import { z } from 'zod'; type MixedZodError<A, B> = Opaque<{ a: A; b: B; }, 'MixedZodError'>; /** * The path with colon-prefixed parameters * e.g. "/posts/:id". */ type Path = string; declare const NullSymbol: unique symbol; export declare const ContractNoBody: unique symbol; export type ContractPlainType<T> = Opaque<T, 'ContractPlainType'>; export type ContractNullType = Opaque<typeof NullSymbol, 'ContractNullType'>; export type ContractNoBodyType = typeof ContractNoBody; export type ContractAnyType = z.ZodSchema | ContractPlainType<unknown> | ContractNullType | null; export type ContractOtherResponse<T extends ContractAnyType> = Opaque<{ contentType: string; body: T; }, 'ContractOtherResponse'>; export type AppRouteResponse = ContractAnyType | ContractNoBodyType | ContractOtherResponse<ContractAnyType>; type AppRouteCommon = { path: Path; pathParams?: ContractAnyType; query?: ContractAnyType; headers?: ContractAnyType; summary?: string; description?: string; deprecated?: boolean; responses: Record<number, AppRouteResponse>; strictStatusCodes?: boolean; metadata?: unknown; /** * @deprecated Use `validateResponse` on the client options */ validateResponseOnClient?: boolean; }; /** * A query endpoint. In REST terms, one using GET. */ export type AppRouteQuery = AppRouteCommon & { method: 'GET'; }; /** * A mutation endpoint. In REST terms, one using POST, PUT, * PATCH, or DELETE. */ export type AppRouteMutation = AppRouteCommon & { method: 'POST' | 'DELETE' | 'PUT' | 'PATCH'; contentType?: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded'; body: ContractAnyType | ContractNoBodyType; }; /** * A mutation endpoint. In REST terms, one using POST, PUT, * PATCH, or DELETE. */ export type AppRouteDeleteNoBody = AppRouteCommon & { method: 'DELETE'; }; type ValidatedHeaders<T extends AppRoute, TOptions extends RouterOptions, TOptionsApplied = ApplyOptions<T, TOptions>> = 'headers' extends keyof TOptionsApplied ? TOptionsApplied['headers'] extends MixedZodError<infer A, infer B> ? { _error: 'Cannot mix plain object types with Zod objects for headers'; a: A; b: B; } : T : T; /** * Recursively process a router, allowing for you to define nested routers. * * The main purpose of this is to convert all path strings into string constants so we can infer the path */ type RecursivelyProcessAppRouter<T extends AppRouter, TOptions extends RouterOptions> = { [K in keyof T]: T[K] extends AppRoute ? ValidatedHeaders<T[K], TOptions> : T[K] extends AppRouter ? RecursivelyProcessAppRouter<T[K], TOptions> : T[K]; }; type RecursivelyApplyOptions<TRouter extends AppRouter, TOptions extends RouterOptions> = { [TRouterKey in keyof TRouter]: TRouter[TRouterKey] extends AppRoute ? Prettify<ApplyOptions<TRouter[TRouterKey], TOptions>> : TRouter[TRouterKey] extends AppRouter ? RecursivelyApplyOptions<TRouter[TRouterKey], TOptions> : TRouter[TRouterKey]; }; type UniversalMerge<A, B> = A extends z.AnyZodObject ? B extends z.AnyZodObject ? z.ZodObject<z.objectUtil.MergeShapes<A['shape'], B['shape']>, B['_def']['unknownKeys'], B['_def']['catchall']> : unknown extends B ? A : MixedZodError<A, B> : unknown extends A ? B : B extends z.AnyZodObject ? MixedZodError<A, B> : unknown extends B ? A : Prettify<Merge<A extends ContractPlainType<infer APlain> ? APlain : A, B extends ContractPlainType<infer BPlain> ? BPlain : B>>; type ApplyOptions<TRoute extends AppRoute, TOptions extends RouterOptions> = Omit<TRoute, 'headers' | 'path' | 'responses'> & WithoutUnknown<{ path: TOptions['pathPrefix'] extends string ? `${TOptions['pathPrefix']}${TRoute['path']}` : TRoute['path']; headers: UniversalMerge<TOptions['baseHeaders'], TRoute['headers']>; strictStatusCodes: TRoute['strictStatusCodes'] extends boolean ? TRoute['strictStatusCodes'] : TOptions['strictStatusCodes'] extends boolean ? TOptions['strictStatusCodes'] : unknown; responses: 'commonResponses' extends keyof TOptions ? Prettify<Merge<TOptions['commonResponses'], TRoute['responses']>> : TRoute['responses']; metadata: 'metadata' extends keyof TOptions ? Prettify<Merge<TOptions['metadata'], TRoute['metadata']>> : TRoute['metadata']; }>; /** * A union of all possible endpoint types. */ export type AppRoute = AppRouteQuery | AppRouteMutation | AppRouteDeleteNoBody; export type AppRouteStrictStatusCodes = Omit<AppRoute, 'strictStatusCodes'> & { strictStatusCodes: true; }; /** * A router (or contract) in @ts-rest is a collection of more routers or * individual routes */ export type AppRouter = { [key: string]: AppRouter | AppRoute; }; export type FlattenAppRouter<T extends AppRouter | AppRoute> = T extends AppRoute ? T : { [TKey in keyof T]: T[TKey] extends AppRoute ? T[TKey] : T[TKey] extends AppRouter ? FlattenAppRouter<T[TKey]> : never; }[keyof T]; export type RouterOptions<TPrefix extends string = string> = { baseHeaders?: unknown; strictStatusCodes?: boolean; pathPrefix?: TPrefix; commonResponses?: Record<number, AppRouteResponse>; metadata?: unknown; /** * @deprecated Use `validateResponse` on the client options */ validateResponseOnClient?: boolean; }; /** * Differentiate between a route and a router * * @param obj * @returns */ export declare const isAppRoute: (obj: AppRoute | AppRouter) => obj is AppRoute; export declare const isAppRouteQuery: (route: AppRoute) => route is AppRouteQuery; export declare const isAppRouteMutation: (route: AppRoute) => route is AppRouteMutation; type NarrowObject<T> = { [K in keyof T]: T[K]; }; /** * The instantiated ts-rest client */ type ContractInstance = { /** * A collection of routes or routers */ router: <TRouter extends AppRouter, TPrefix extends string, TOptions extends RouterOptions<TPrefix> = {}>(endpoints: RecursivelyProcessAppRouter<TRouter, TOptions>, options?: TOptions) => RecursivelyApplyOptions<TRouter, TOptions>; /** * A single query route, should exist within * a {@link AppRouter} */ query: <T extends AppRouteQuery>(query: NarrowObject<T>) => T; /** * A single mutation route, should exist within * a {@link AppRouter} */ mutation: <T extends AppRouteMutation>(mutation: NarrowObject<T>) => T; responses: <TResponses extends Record<number, AppRouteResponse>>(responses: TResponses) => TResponses; /** * @deprecated Please use type() instead. */ response: <T>() => T extends null ? ContractNullType : ContractPlainType<T>; /** * @deprecated Please use type() instead. */ body: <T>() => T extends null ? ContractNullType : ContractPlainType<T>; /** * Exists to allow storing a Type in the contract (at compile time only) */ type: <T>() => T extends null ? ContractNullType : ContractPlainType<T>; /** * Define a custom response type */ otherResponse: <T extends ContractAnyType>({ contentType, body, }: { contentType: string; body: T; }) => ContractOtherResponse<T>; /** Use to indicate that a route takes no body or responds with no body */ noBody: () => ContractNoBodyType; }; /** * * @deprecated Please use {@link initContract} instead. */ export declare const initTsRest: () => ContractInstance; export declare const ContractPlainTypeRuntimeSymbol: any; /** * Instantiate a ts-rest client, primarily to access `router`, `response`, and `body` * * @returns {ContractInstance} */ export declare const initContract: () => ContractInstance; export {};