@ts-rest/core
Version:
RPC-like experience over a regular REST API, with type safe server implementations 🪄
179 lines (178 loc) • 7.71 kB
TypeScript
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 {};