Skip to content

Commit 2bb39f8

Browse files
authored
feat: add type inference helpers (#193)
* feat: add inference type helpers
1 parent a3bdddb commit 2bb39f8

9 files changed

Lines changed: 480 additions & 34 deletions

File tree

.changeset/purple-comics-notice.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/core': minor
3+
---
4+
5+
Added inference type helpers

apps/docs/docs/core/core.md

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,3 @@ export const contract = c.router({
116116
},
117117
});
118118
```
119-
120-
## Response Types
121-
122-
If you need to quickly extract the Response types from a given Contract, you can use the `ResponsesForRouter` type.
123-
124-
```typescript
125-
import { ResponsesForRouter } from '@ts-rest/core'
126-
import { contract } from './contract'
127-
128-
// initContract() must called prior
129-
type ResponseShapes = ResponsesForRouter<typeof contract>
130-
131-
async function someHttpCall(req: Request): Promise<ResponseShapes['getPosts']> {
132-
return ...
133-
}
134-
```
135-
136-
This is particularly useful for Services/Functions/Lambdas that do not use frameworks like Express or Nestjs.

apps/docs/docs/core/form-data.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Tabs from '@theme/Tabs';
22
import TabItem from '@theme/TabItem';
33

4-
# multipart/form-data
4+
# File Uploading
55

66
ts-rest supports multipart/form-data requests, this is useful for uploading files or working with FormData from a form.
77

apps/docs/docs/core/infer-types.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Inferring Types
2+
3+
Often, we need to manually extract the request or responses types of specific contract endpoints, so functions, services, lambdas, React components, etc. can be safely typed
4+
when there is no automatic type inference.
5+
6+
We have separate type helpers for server-side and client-side code since we need to infer either the Input or Output Zod types depending on
7+
where the code is used.
8+
9+
## Inferring Response Types
10+
11+
To get the response types of a contract or a specific endpoint, we have the following type helpers:
12+
13+
- `ServerInferResponses<AppRouter | AppRoute, OptionalHttpStatusCode>`
14+
- `ClientInferResponses<AppRouter | AppRoute, OptionalHttpStatusCode>`
15+
16+
```typescript
17+
import { ServerInferResponses } from '@ts-rest/core';
18+
import { contract } from './contract';
19+
20+
type ResponseShapes = ServerInferResponses<typeof contract>;
21+
22+
async function someHttpCall(req: Request): Promise<ResponseShapes['getPosts']> {
23+
return ...;
24+
}
25+
26+
function someServiceCall(): ServerInferResponses<typeof contract.getPosts> {
27+
return ...;
28+
}
29+
```
30+
31+
### Inferring Response Body
32+
33+
If you need to infer the response body for a defined response status of a specific endpoint, we can use the following type helpers:
34+
35+
- `ServerInferResponseBody<AppRoute, OptionalHttpStatusCode>`
36+
- `ClientInferResponseBody<AppRoute, OptionalHttpStatusCode>`
37+
38+
This is syntactic sugar for `ServerInferResponses<AppRoute, OptionalHttpStatusCode & keyof AppRoute['responses']>['body']`
39+
40+
```typescript
41+
import React from 'react';
42+
import { ClientInferResponseBody } from '@ts-rest/core';
43+
import { contract } from './contract';
44+
45+
type Post = ClientInferResponseBody<typeof contract.getPost, 200>;
46+
47+
function PostComponent(props: { post: Post }) {
48+
return <>...</>;
49+
}
50+
```
51+
52+
## Inferring Request Types
53+
54+
To get the request (path params, query params, body) types of a contract or a specific endpoint, we have the following type helpers:
55+
56+
- `ServerInferRequest<AppRouter | AppRoute>`
57+
- `ClientInferRequest<AppRouter | AppRoute>`
58+
59+
```typescript
60+
import { ServerInferRequest, ServerInferResponses } from '@ts-rest/core';
61+
import { contract } from './contract';
62+
63+
type GetPostRequest = ServerInferRequest<typeof contract.getPost>;
64+
type GetPostResponse = ServerInferResponses<typeof contract.getPost>;
65+
66+
async function getPostLambdaHandler({ params, query }: GetPostRequest): Promise<GetPostResponse> {
67+
return ...;
68+
}
69+
```

apps/docs/sidebars.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const sidebars = {
6666
{ type: 'doc', id: 'core/core' },
6767
{ type: 'doc', id: 'core/fetch' },
6868
{ type: 'doc', id: 'core/custom' },
69+
{ type: 'doc', id: 'core/infer-types' },
6970
{ type: 'doc', id: 'core/errors' },
7071
{ type: 'doc', id: 'core/form-data' },
7172
],

libs/ts-rest/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './lib/type-utils';
77
export * from './lib/zod-utils';
88
export * from './lib/server';
99
export * from './lib/response-validation-error';
10+
export * from './lib/infer-types';

libs/ts-rest/core/src/lib/client.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,17 @@ export type PathParamsFromUrl<T extends AppRoute> = ParamsFromUrl<
3434
/**
3535
* Merge `PathParamsFromUrl<T>` with pathParams schema if it exists
3636
*/
37-
export type PathParamsWithCustomValidators<T extends AppRoute> =
38-
T['pathParams'] extends undefined
39-
? PathParamsFromUrl<T>
40-
: Merge<PathParamsFromUrl<T>, ZodInferOrType<T['pathParams']>>;
37+
export type PathParamsWithCustomValidators<
38+
T extends AppRoute,
39+
TClientOrServer extends 'client' | 'server' = 'server'
40+
> = T['pathParams'] extends undefined
41+
? PathParamsFromUrl<T>
42+
: Merge<
43+
PathParamsFromUrl<T>,
44+
TClientOrServer extends 'server'
45+
? ZodInferOrType<T['pathParams']>
46+
: ZodInputOrType<T['pathParams']>
47+
>;
4148

4249
// Allow FormData if the contentType is multipart/form-data
4350
type AppRouteBodyOrFormData<T extends AppRouteMutation> =
@@ -91,24 +98,20 @@ export type ApiRouteResponse<T> =
9198
body: unknown;
9299
};
93100

94-
export type ResponseForRoute<T extends AppRoute> = ApiRouteResponse<
101+
/**
102+
* @deprecated Only safe to use on the client-side. Use `ServerInferResponses`/`ClientInferResponses` instead.
103+
*/
104+
export type ApiResponseForRoute<T extends AppRoute> = ApiRouteResponse<
95105
T['responses']
96-
>
97-
98-
export type ResponsesForRouter<T extends AppRouter> = {
99-
[K in keyof T]: T[K] extends AppRoute
100-
? ResponseForRoute<T[K]>
101-
: T[K] extends AppRouter ? ResponsesForRouter<T[K]> : never;
102-
};
106+
>;
103107

104108
/**
105-
*
106-
* @deprecated
109+
* @deprecated Only safe to use on the client-side. Use `ServerInferResponses`/`ClientInferResponses` instead.
107110
*/
108111
export function getRouteResponses<T extends AppRouter>(router: T) {
109112
return {} as {
110113
[K in keyof typeof router]: typeof router[K] extends AppRoute
111-
? ResponseForRoute<typeof router[K]>
114+
? ApiResponseForRoute<typeof router[K]>
112115
: 'not a route';
113116
};
114117
}

0 commit comments

Comments
 (0)