TypeScript Types
Master the TypeScript types that make endpoint-fetcher fully type-safe.
Type Parameters
All endpoint helpers and the endpoint() function accept three generic type parameters:
get<TInput, TOutput, TError>()
post<TInput, TOutput, TError>()
put<TInput, TOutput, TError>()
patch<TInput, TOutput, TError>()
del<TInput, TOutput, TError>()
endpoint<TInput, TOutput, TError>()TInput - Request Parameters
The shape of data passed to the endpoint:
// No input needed
const listUsers = get<void, User[]>('/users');
// Simple input
const getUser = get<{ id: string }, User>((input) => `/users/${input.id}`);
// Complex input
type SearchParams = {
query: string;
filters?: {
status?: 'active' | 'inactive';
role?: 'admin' | 'user';
};
page?: number;
limit?: number;
};
const searchUsers = get<SearchParams, User[]>((input) => {
const params = new URLSearchParams({ q: input.query });
if (input.page) params.set('page', input.page.toString());
return `/users/search?${params}`;
});
// POST with body
type CreateUserInput = {
name: string;
email: string;
role: 'admin' | 'user';
};
const createUser = post<CreateUserInput, User>('/users');TOutput - Response Type
The shape of data returned from the endpoint:
// Simple response
type User = {
id: string;
name: string;
email: string;
};
const getUser = get<{ id: string }, User>((input) => `/users/${input.id}`);
// Array response
const listUsers = get<void, User[]>('/users');
// Paginated response
type PaginatedResponse<T> = {
data: T[];
total: number;
page: number;
pageSize: number;
};
const listUsersPaginated = get<{ page: number }, PaginatedResponse<User>>(
(input) => `/users?page=${input.page}`
);
// No response (void)
const deleteUser = del<{ id: string }, void>((input) => `/users/${input.id}`);TError - Error Type
The shape of error responses (defaults to any):
// Simple error
type ApiError = {
message: string;
};
const getUser = get<{ id: string }, User, ApiError>(
(input) => `/users/${input.id}`
);
// Detailed error
type ValidationError = {
message: string;
code: string;
errors: Array<{
field: string;
message: string;
}>;
};
const createUser = post<CreateUserInput, User, ValidationError>('/users');
// Different errors per endpoint
type NotFoundError = { message: string; resourceId: string };
type AuthError = { message: string; code: 'UNAUTHORIZED' | 'FORBIDDEN' };
const api = createApiClient({
users: group({
endpoints: {
getById: get<{ id: string }, User, NotFoundError>(
(input) => `/users/${input.id}`
),
}
}),
auth: group({
endpoints: {
login: post<LoginInput, AuthResponse, AuthError>('/auth/login'),
}
}),
}, {
baseUrl: 'https://api.example.com',
});Helper Function Signatures
get<TInput, TOutput, TError>()
function get<TInput = void, TOutput = any, TError = any>(
path: string | ((input: TInput) => string),
handler?: CustomHandler<TInput, TOutput>,
hooks?: Hooks
): EndpointConfig<TInput, TOutput, TError>Examples:
// Minimal
const list = get<void, User[]>('/users');
// With input
const getById = get<{ id: string }, User>((input) => `/users/${input.id}`);
// With error type
const getById = get<{ id: string }, User, ApiError>((input) => `/users/${input.id}`);
// With custom handler
const download = get<{ id: string }, Blob>(
(input) => `/files/${input.id}`,
async ({ fetch, baseUrl, path }) => {
const response = await fetch(`${baseUrl}${path}`, { method: 'GET' });
return response.blob();
}
);
// With hooks
const create = get<void, User[]>(
'/users',
undefined,
{
beforeRequest: async (url, init) => ({ url, init }),
}
);post<TInput, TOutput, TError>()
function post<TInput, TOutput = any, TError = any>(
path: string | ((input: TInput) => string),
handler?: CustomHandler<TInput, TOutput>,
hooks?: Hooks
): EndpointConfig<TInput, TOutput, TError>Note: TInput is required (no default) since POST usually needs a body.
put<TInput, TOutput, TError>()
Same signature as post().
patch<TInput, TOutput, TError>()
Same signature as post().
del<TInput, TOutput, TError>()
function del<TInput = void, TOutput = any, TError = any>(
path: string | ((input: TInput) => string),
handler?: CustomHandler<TInput, TOutput>,
hooks?: Hooks
): EndpointConfig<TInput, TOutput, TError>Note: TInput defaults to void since DELETE often only needs URL parameters.
endpoint<TInput, TOutput, TError>()
function endpoint<TInput = any, TOutput = any, TError = any>(
config: EndpointConfig<TInput, TOutput, TError>
): EndpointConfig<TInput, TOutput, TError>Example:
const searchUsers = endpoint<SearchInput, User[], ApiError>({
method: 'POST',
path: '/users/search',
handler: async ({ input, fetch, baseUrl, path }) => {
const response = await fetch(`${baseUrl}${path}`, {
method: 'POST',
body: JSON.stringify(input),
});
return response.json();
},
});group<T>()
function group<T extends GroupConfig>(config: T): TExample:
const usersGroup = group({
hooks: {
beforeRequest: async (url, init) => ({ url, init }),
},
endpoints: {
list: get<void, User[]>('/users'),
},
});Type Safety Examples
Input Type Safety
TypeScript enforces the input type:
const api = createApiClient({
users: group({
endpoints: {
getById: get<{ id: string }, User>((input) => `/users/${input.id}`),
}
}),
}, {
baseUrl: 'https://api.example.com',
});
// ✅ Correct
await api.users.getById({ id: '123' });
// ❌ TypeScript error: Property 'id' is missing
await api.users.getById({});
// ❌ TypeScript error: Type 'number' is not assignable to type 'string'
await api.users.getById({ id: 123 });Output Type Safety
TypeScript infers the return type:
type User = {
id: string;
name: string;
email: string;
};
const api = createApiClient({
users: group({
endpoints: {
getById: get<{ id: string }, User>((input) => `/users/${input.id}`),
}
}),
}, {
baseUrl: 'https://api.example.com',
});
const user = await api.users.getById({ id: '123' });
// ✅ TypeScript knows these properties exist
console.log(user.id);
console.log(user.name);
console.log(user.email);
// ❌ TypeScript error: Property 'foo' does not exist
console.log(user.foo);Error Type Safety
Error types are documented via the third parameter:
type ValidationError = {
message: string;
errors: Array<{ field: string; message: string }>;
};
const api = createApiClient({
users: group({
endpoints: {
create: post<CreateUserInput, User, ValidationError>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
});
try {
await api.users.create({ name: 'John', email: 'invalid' });
} catch (err: unknown) {
const error = err as { status: number; error: ValidationError };
// ✅ TypeScript knows the error shape
console.log(error.error.message);
error.error.errors.forEach(e => {
console.log(`${e.field}: ${e.message}`);
});
}Autocomplete Throughout
TypeScript provides autocomplete at every level:
const api = createApiClient({
admin: group({
groups: {
users: group({
endpoints: {
list: get<void, User[]>('/admin/users'),
ban: post<{ id: string }, void>((input) => `/admin/users/${input.id}/ban`),
}
}),
}
}),
}, {
baseUrl: 'https://api.example.com',
});
// Autocomplete suggests:
api. // → admin
api.admin. // → users
api.admin.users. // → list, ban
// Autocomplete for inputs:
api.admin.users.ban({ id: '123' }); // → suggests 'id' propertyBest Practices
Define Clear Types
Create well-named types for inputs and outputs:
// ✅ Good - clear, reusable types
type CreateUserInput = {
name: string;
email: string;
role: 'admin' | 'user';
};
type User = {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: string;
};
type ApiError = {
message: string;
code: string;
};
const createUser = post<CreateUserInput, User, ApiError>('/users');
// ❌ Avoid - inline types are hard to maintain
const createUser = post<
{ name: string; email: string; role: 'admin' | 'user' },
{ id: string; name: string; email: string; role: 'admin' | 'user'; createdAt: string },
{ message: string; code: string }
>('/users');Use Utility Types
Leverage TypeScript utility types:
type User = {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: string;
updatedAt: string;
};
// Omit fields not needed for creation
type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
// Partial for updates
type UpdateUserInput = Partial<User> & { id: string };
// Pick specific fields
type UserSummary = Pick<User, 'id' | 'name'>;
const api = createApiClient({
users: group({
endpoints: {
create: post<CreateUserInput, User>('/users'),
update: patch<UpdateUserInput, User>((input) => `/users/${input.id}`),
getSummary: get<{ id: string }, UserSummary>(
(input) => `/users/${input.id}/summary`
),
}
}),
}, {
baseUrl: 'https://api.example.com',
});Reusable Type Patterns
Create reusable generic types:
// Paginated response
type Paginated<T> = {
data: T[];
total: number;
page: number;
pageSize: number;
};
// API response wrapper
type ApiResponse<T> = {
success: boolean;
data: T;
timestamp: string;
};
// Usage
const listUsers = get<{ page: number }, Paginated<User>>(
(input) => `/users?page=${input.page}`
);
const getUser = get<{ id: string }, ApiResponse<User>>(
(input) => `/users/${input.id}`
);Discriminated Unions for Errors
Use discriminated unions for different error types:
type ValidationError = {
type: 'validation';
message: string;
errors: Array<{ field: string; message: string }>;
};
type AuthError = {
type: 'auth';
message: string;
code: 'UNAUTHORIZED' | 'FORBIDDEN';
};
type NotFoundError = {
type: 'not_found';
message: string;
resourceId: string;
};
type ApiError = ValidationError | AuthError | NotFoundError;
const api = createApiClient({
users: group({
endpoints: {
create: post<CreateUserInput, User, ApiError>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
});
try {
await api.users.create({ name: 'John', email: 'invalid' });
} catch (err: unknown) {
const error = err as { status: number; error: ApiError };
// TypeScript narrows the type based on discriminator
switch (error.error.type) {
case 'validation':
error.error.errors.forEach(e => {
console.log(`${e.field}: ${e.message}`);
});
break;
case 'auth':
console.log('Auth error:', error.error.code);
break;
case 'not_found':
console.log('Not found:', error.error.resourceId);
break;
}
}Const Assertions for Literals
Use const assertions for literal types:
const ROLES = ['admin', 'user', 'moderator'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'user' | 'moderator'
type User = {
id: string;
name: string;
role: Role;
};
const STATUS = {
ACTIVE: 'active',
INACTIVE: 'inactive',
PENDING: 'pending',
} as const;
type Status = typeof STATUS[keyof typeof STATUS];
type SearchParams = {
status?: Status;
role?: Role;
};Type Inference
Inferred Return Types
Let TypeScript infer return types:
const api = createApiClient({
users: group({
endpoints: {
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
});
// TypeScript infers: Promise<User[]>
const users = await api.users.list();
// No need to explicitly type:
// const users: User[] = await api.users.list();Extract Types from API Client
Extract types from your API client:
const api = createApiClient({
users: group({
endpoints: {
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
});
// Extract the type of a specific endpoint
type ListUsersResponse = Awaited<ReturnType<typeof api.users.list>>;
// Result: User[]
// Extract input type
type ListUsersInput = Parameters<typeof api.users.list>[0];
// Result: voidCommon Patterns
Optional Fields
type CreateUserInput = {
name: string;
email: string;
bio?: string; // Optional
avatar?: string; // Optional
};Readonly Fields
type User = {
readonly id: string; // Cannot be modified
readonly createdAt: string; // Cannot be modified
name: string;
email: string;
};Enums vs Union Types
// Union types (preferred)
type Role = 'admin' | 'user' | 'moderator';
// Enums (if you need runtime values)
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending',
}
type User = {
id: string;
role: Role;
status: Status;
};Generic Endpoints
function createCRUD<T, TCreate = Omit<T, 'id'>, TUpdate = Partial<T> & { id: string }>() {
return group({
endpoints: {
list: get<void, T[]>('/resource'),
getById: get<{ id: string }, T>((input) => `/resource/${input.id}`),
create: post<TCreate, T>('/resource'),
update: patch<TUpdate, T>((input) => `/resource/${input.id}`),
delete: del<{ id: string }, void>((input) => `/resource/${input.id}`),
}
});
}
// Usage
const users = createCRUD<User>();Next Steps
Now that you understand TypeScript types:
- Error Handling - Type-safe error handling
- Custom Handlers - Type handlers correctly
- Advanced Examples - Complex type patterns