🚧

Alpha Release - This project is in early development and APIs may change

ts-contractts-contract
API ReferenceCore API

Type Helpers

Complete API reference for TypeScript type inference helpers.

Overview

ts-contract provides type helpers that extract TypeScript types from your route definitions. These enable end-to-end type safety without code generation.

All type helpers are compile-time only - they have zero runtime overhead.

Import

import type {
  InferPathParams,
  InferQuery,
  InferBody,
  InferHeaders,
  InferResponseBody,
  InferResponses,
  InferArgs,
} from '@ts-contract/core';

Type Helpers Reference

InferPathParams

Extract path parameter types from a route.

type InferPathParams<R extends RouteDef>

Type Parameters:

  • R - A route definition type

Returns:

  • Object type with path parameter properties, or undefined if no pathParams

Example:

const contract = createContract({
  getPost: {
    method: 'GET',
    path: '/users/:userId/posts/:postId',
    pathParams: z.object({
      userId: z.string(),
      postId: z.string(),
    }),
    responses: {
      200: z.object({ id: z.string() }),
    },
  },
});

type Params = InferPathParams<typeof contract.getPost>;
// => { userId: string; postId: string }

Usage:

function handleRequest(params: Params) {
  console.log(params.userId, params.postId);
}

InferQuery

Extract query parameter types from a route.

type InferQuery<R extends RouteDef>

Type Parameters:

  • R - A route definition type

Returns:

  • Object type with query parameter properties, or undefined if no query

Example:

const contract = createContract({
  listUsers: {
    method: 'GET',
    path: '/users',
    query: z.object({
      page: z.string().optional(),
      limit: z.string().optional(),
      sort: z.enum(['asc', 'desc']).optional(),
    }),
    responses: {
      200: z.array(z.object({ id: z.string() })),
    },
  },
});

type Query = InferQuery<typeof contract.listUsers>;
// => { page?: string; limit?: string; sort?: 'asc' | 'desc' }

Usage:

function buildQueryString(query: Query): string {
  const params = new URLSearchParams();
  if (query.page) params.set('page', query.page);
  if (query.limit) params.set('limit', query.limit);
  if (query.sort) params.set('sort', query.sort);
  return params.toString();
}

InferBody

Extract request body type from a route.

type InferBody<R extends RouteDef>

Type Parameters:

  • R - A route definition type

Returns:

  • Request body type, or undefined if no body

Example:

const contract = createContract({
  createUser: {
    method: 'POST',
    path: '/users',
    body: z.object({
      name: z.string(),
      email: z.string().email(),
      age: z.number().int().min(0),
    }),
    responses: {
      201: z.object({ id: z.string() }),
    },
  },
});

type Body = InferBody<typeof contract.createUser>;
// => { name: string; email: string; age: number }

Usage:

async function createUser(body: Body) {
  const response = await fetch('/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  return response.json();
}

InferHeaders

Extract request header types from a route.

type InferHeaders<R extends RouteDef>

Type Parameters:

  • R - A route definition type

Returns:

  • Object type with header properties, or undefined if no headers

Example:

const contract = createContract({
  getProtected: {
    method: 'GET',
    path: '/protected',
    headers: {
      'authorization': z.string(),
      'x-api-key': z.string(),
    },
    responses: {
      200: z.object({ data: z.string() }),
    },
  },
});

type Headers = InferHeaders<typeof contract.getProtected>;
// => { authorization: string; 'x-api-key': string }

Usage:

async function fetchProtected(headers: Headers) {
  const response = await fetch('/protected', { headers });
  return response.json();
}

InferResponseBody

Extract a specific response type by status code.

type InferResponseBody<R extends RouteDef, S extends HttpStatusCodes>

Type Parameters:

  • R - A route definition type
  • S - An HTTP status code

Returns:

  • Response body type for the specified status code

Example:

const contract = createContract({
  getUser: {
    method: 'GET',
    path: '/users/:id',
    pathParams: z.object({ id: z.string() }),
    responses: {
      200: z.object({
        id: z.string(),
        name: z.string(),
        email: z.string(),
      }),
      404: z.object({
        message: z.string(),
      }),
      500: z.object({
        message: z.string(),
        code: z.string(),
      }),
    },
  },
});

type SuccessResponse = InferResponseBody<typeof contract.getUser, 200>;
// => { id: string; name: string; email: string }

type NotFoundResponse = InferResponseBody<typeof contract.getUser, 404>;
// => { message: string }

type ErrorResponse = InferResponseBody<typeof contract.getUser, 500>;
// => { message: string; code: string }

Usage:

async function getUser(id: string): Promise<SuccessResponse> {
  const response = await fetch(`/users/${id}`);
  
  if (response.status === 404) {
    const error: NotFoundResponse = await response.json();
    throw new Error(error.message);
  }
  
  if (!response.ok) {
    throw new Error('Request failed');
  }
  
  const user: SuccessResponse = await response.json();
  return user;
}

InferResponses

Extract all responses as a discriminated union.

type InferResponses<R extends RouteDef>

Type Parameters:

  • R - A route definition type

Returns:

  • Discriminated union of { status: StatusCode; body: ResponseBody } for all responses

Example:

const contract = createContract({
  getUser: {
    method: 'GET',
    path: '/users/:id',
    pathParams: z.object({ id: z.string() }),
    responses: {
      200: z.object({ id: z.string(), name: z.string() }),
      404: z.object({ message: z.string() }),
      500: z.object({ message: z.string() }),
    },
  },
});

type Response = InferResponses<typeof contract.getUser>;
// => 
// | { status: 200; body: { id: string; name: string } }
// | { status: 404; body: { message: string } }
// | { status: 500; body: { message: string } }

Usage:

function handleResponse(response: Response) {
  switch (response.status) {
    case 200:
      // response.body is { id: string; name: string }
      console.log('User:', response.body.name);
      break;
    case 404:
      // response.body is { message: string }
      console.error('Not found:', response.body.message);
      break;
    case 500:
      // response.body is { message: string }
      console.error('Server error:', response.body.message);
      break;
  }
}

InferArgs

Merge all input types (params, query, body, headers) into a single object.

type InferArgs<R extends RouteDef>

Type Parameters:

  • R - A route definition type

Returns:

  • Object with optional params, query, body, and headers properties

Example:

const contract = createContract({
  updateUser: {
    method: 'PUT',
    path: '/users/:id',
    pathParams: z.object({ id: z.string() }),
    query: z.object({ notify: z.boolean().optional() }),
    body: z.object({
      name: z.string(),
      email: z.string(),
    }),
    responses: {
      200: z.object({ id: z.string() }),
    },
  },
});

type Args = InferArgs<typeof contract.updateUser>;
// => {
//   params: { id: string };
//   query: { notify?: boolean };
//   body: { name: string; email: string };
// }

Usage:

function buildUpdateRequest(args: Args): Request {
  const url = `/users/${args.params.id}${
    args.query.notify ? '?notify=true' : ''
  }`;
  
  return new Request(url, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(args.body),
  });
}

// Usage
const request = buildUpdateRequest({
  params: { id: '123' },
  query: { notify: true },
  body: { name: 'Alice', email: '[email protected]' },
});

WebSocket Type Helpers

ts-contract provides type helpers for WebSocket contracts:

InferWebSocketPathParams

Extract path parameter types from a WebSocket definition:

import type { InferWebSocketPathParams } from '@ts-contract/core';

type Params = InferWebSocketPathParams<typeof contract.chat>;
// => { roomId: string }

InferWebSocketQuery

Extract query parameter types:

import type { InferWebSocketQuery } from '@ts-contract/core';

type Query = InferWebSocketQuery<typeof contract.chat>;
// => { token?: string }

InferWebSocketHeaders

Extract header types:

import type { InferWebSocketHeaders } from '@ts-contract/core';

type Headers = InferWebSocketHeaders<typeof contract.chat>;
// => { authorization: string }

InferClientMessages

Extract all client message types:

import type { InferClientMessages } from '@ts-contract/core';

type ClientMsgs = InferClientMessages<typeof contract.chat>;
// => { new_msg: { type: 'new_msg', body: string }, ... }

InferServerMessages

Extract all server message types:

import type { InferServerMessages } from '@ts-contract/core';

type ServerMsgs = InferServerMessages<typeof contract.chat>;
// => { new_msg: { type: 'new_msg', id: string, body: string }, ... }

InferClientMessage

Extract a specific client message type:

import type { InferClientMessage } from '@ts-contract/core';

type NewMsg = InferClientMessage<typeof contract.chat, 'new_msg'>;
// => { type: 'new_msg', body: string }

InferServerMessage

Extract a specific server message type:

import type { InferServerMessage } from '@ts-contract/core';

type NewMsg = InferServerMessage<typeof contract.chat, 'new_msg'>;
// => { type: 'new_msg', id: string, body: string }

Usage:

type Params = InferWebSocketPathParams<typeof contract.chat>;
type ClientMsg = InferClientMessage<typeof contract.chat, 'new_msg'>;
type ServerMsg = InferServerMessage<typeof contract.chat, 'new_msg'>;

Template Literal Types

type UserId = InferPathParams<typeof contract.getUser>['id'];

type UserKey = `user:${UserId}`;
// => "user:string"

Extracting Nested Types

type UserList = InferResponseBody<typeof contract.listUsers, 200>;
type User = UserList extends Array<infer U> ? U : never;
// Extract the array element type

Best Practices

1. Use Type Aliases

Create meaningful type aliases for reuse:

// types.ts
export type User = InferResponseBody<typeof contract.getUser, 200>;
export type UserList = InferResponseBody<typeof contract.listUsers, 200>;
export type CreateUserPayload = InferBody<typeof contract.createUser>;

2. Colocate with Usage

Define types close to where they're used:

// user-service.ts
import type { InferResponseBody } from '@ts-contract/core';
import { contract } from './contract';

type User = InferResponseBody<typeof contract.getUser, 200>;

export class UserService {
  async getUser(id: string): Promise<User> {
    // ...
  }
}

3. Use typeof Correctly

Always use typeof when referencing contract routes:

// ✓ Correct
type User = InferResponseBody<typeof contract.getUser, 200>;

// ✗ Wrong
type User = InferResponseBody<contract.getUser, 200>;

4. Avoid any

Don't cast to any - use proper type inference:

// ✗ Avoid
const user = await response.json() as any;

// ✓ Better
type User = InferResponseBody<typeof contract.getUser, 200>;
const user = await response.json() as User;

See Also

Next Steps