Skip to content

Basic Usage

Simple GET Request

typescript
import { typedFetch, NotFoundError, UnauthorizedError } from '@pbpeterson/typed-fetch';

interface User {
  id: number;
  name: string;
  email: string;
}

type ExpectedErrors = NotFoundError | UnauthorizedError;
const { response, error } = await typedFetch<User[], ExpectedErrors>('/api/users');

if (error) {
  if (error instanceof NotFoundError) {
    console.log('Users endpoint not found');
  } else if (error instanceof UnauthorizedError) {
    console.log('Authentication required');
  } else {
    console.error('Unexpected error:', error.statusText);
  }
} else {
  const users = await response.json(); // Type: User[]
}

Specifying Expected Client Errors

Important

Specify which client errors (4xx) you expect. Without the second generic, the error union includes all 29 client error types, making handling verbose.

typescript
// ❌ Without specifying — includes ALL client errors
const { error } = await typedFetch<User[]>('/api/users');
// error: BadRequestError | UnauthorizedError | ... (29 types) | ServerErrors | NetworkError | null

// ✅ Specify only what the endpoint can return
type ExpectedErrors = NotFoundError | UnauthorizedError;
const { error: betterError } = await typedFetch<User[], ExpectedErrors>('/api/users');
// error: NotFoundError | UnauthorizedError | ServerErrors | NetworkError | null

Why Server Errors and Network Errors are always included:

  • Server Errors (5xx) — Cannot be controlled by the client.
  • Network Errors — Connection issues, timeouts, and DNS failures are always possible.

POST Request with Body

typescript
import { typedFetch, BadRequestError } from '@pbpeterson/typed-fetch';

interface ValidationError {
  field: string;
  message: string;
}

const { response, error } = await typedFetch<User, BadRequestError>('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'John', email: 'john@example.com' }),
});

if (error) {
  if (error instanceof BadRequestError) {
    // json<T>() accepts a type parameter for typed error bodies
    const errors = await error.json<ValidationError[]>();
    console.error('Validation failed:', errors);
  }
} else {
  const user = await response.json();
}

Using Type Guards

Use isHttpError() and isNetworkError() instead of instanceof for reliable checks, especially across package boundaries:

typescript
import { typedFetch, isHttpError, isNetworkError } from '@pbpeterson/typed-fetch';

const { error } = await typedFetch<User[]>('/api/users');

if (error) {
  if (isHttpError(error)) {
    console.log(`HTTP ${error.status}: ${error.statusText}`);
    console.log('Error name:', error.name); // e.g. "NotFoundError"
  } else if (isNetworkError(error)) {
    console.log('Connection failed:', error.message);
  }
}

Cloning Errors for Multiple Body Reads

typescript
const { error } = await typedFetch<User[]>('/api/users');

if (error && isHttpError(error)) {
  const cloned = error.clone();

  const errorJson = await error.json();
  const errorText = await cloned.text();

  console.log('JSON:', errorJson);
  console.log('Text:', errorText);
}

Request Cancellation and Timeout

Same API as native fetch — all options work:

typescript
// Cancellation
const controller = new AbortController();
const { response, error } = await typedFetch<User[]>('/api/users', {
  signal: controller.signal,
});
controller.abort();

// Timeout
const { response: r, error: e } = await typedFetch<User[]>('/api/users', {
  signal: AbortSignal.timeout(5000),
});

Released under the MIT License.