Typescript - Extend Request
In Hyper-Fetch, you can create highly reusable functions and components by building them to work with generic
RequestInstance objects. This approach lets you enforce a partial shape constraint on a request - for example "any
request whose response is an array of strings" - while letting the caller decide every other detail.
This guide walks you through that pattern. Before diving in, make sure you pick the right tool for the job:
RequestInstance vs RequestModel — pick the right toolHyper Fetch ships two complementary types. They look similar but mean opposite things at the type level. Using the wrong
one silently leaks any into your codebase.
RequestInstance<{...}> (this guide) | RequestModel<{...}> | |
|---|---|---|
| Mindset | "Any request that matches this partial shape." | "This specific endpoint." |
| Use for | Generic constraints, reusable components / hooks / helpers that accept any matching request. | SDK schema leaves, single-endpoint definitions. |
| Omitted fields default to | any - on purpose, so any concrete Request satisfies the constraint. | Strict, non-any (unknown, undefined, Error, string, false). |
| Why those defaults? | "I don't care about that field." Caller picks anything. | "There is no payload / no query / no params here." Mismatches are rejected. |
The rule of thumb:
- Writing a reusable abstraction that accepts requests from outside? →
RequestInstance(this guide). - Defining one endpoint in a schema? →
RequestModel(see the SDK guide).
This guide will walk you through how to extend requests using TypeScript generics to build flexible, type-safe, and modular features.
- How to create generic functions that accept
RequestInstanceobjects. - How to use TypeScript generics to enforce type constraints on requests.
- A practical example of building a reusable autocomplete function.
- The benefits of this pattern for creating type-safe and modular code.
The Core Concept
The key to this pattern is TypeScript's generics. By creating a function or component that accepts a generic type, you can set constraints on what kind of requests are allowed. By default, you can create a generic function that accepts any request instance like this:
import { RequestInstance } from "@hyper-fetch/core";
function someFunction<T extends RequestInstance>(request: T) {
// Accepts ANY Request - response, payload, queryParams, error all default to `any`,
// because you do not care what the caller passes.
}
However, the real power comes from making this more specific. RequestInstance is a generic type itself, so you can
partially constrain it — pin down only the fields you care about and let the rest stay any. For many reusable
components you only need to constrain the response type.
Practical Example: Autocomplete
Let's build on this concept with a generic autocomplete function. We want this function to accept any request that
returns an array of strings (string[]) in its response. Any other response type should result in a TypeScript error.
First, let's define two requests: one that fits our criteria and one that does not.
// We assume 'client' is a pre-configured Client instance
// For example:
// import { createClient } from "@hyper-fetch/core";
// const client = createClient({ url: "..." });
// This request's response is `string[]`, which is what our function expects.
const getSuggestions = client.createRequest<{ response: string[] }>()({
endpoint: "suggestions",
});
// This request's response is `string`, which should be rejected.
const getUsername = client.createRequest<{ response: string }>()({
endpoint: "username",
});
Now, let's create our generic autocomplete function. We'll use a generic constraint to ensure the request parameter
has a response type of string[].
function autocomplete<Request extends RequestInstance<{ response: string[] }>>(request: Request) {
// Now, inside this function, TypeScript knows that `request.send()`
// will eventually return a `data` property of type `string[]` on success.
console.log("Request received, can be used for fetching suggestions.");
return request.send();
}
With this setup, TypeScript will enforce our constraint at compile time.
// ✅ Correct Usage
// This works because `getSuggestions` has a response type of `string[]`.
autocomplete(getSuggestions);
// ⛔ Incorrect Usage
// This will cause a TypeScript error because `getUsername`'s response is `string`, not `string[]`.
autocomplete(getUsername);
When to Use It?
This pattern is powerful for building reusable, type-safe components and functions. Use it for:
- Generic UI Components like
<DataTable />or<SelectField />. - Type-safe utilities for data processing.
- Abstracting business logic into reusable functions.
- Mock / interceptor / configuration callbacks in
sdk.$configure({...}).
By using this approach, you promote code reuse and maintain strong type safety across your application.
Not for schema definitions
If you find yourself reaching for RequestInstance inside a schema declaration like this:
// ⛔ This silently widens response, payload, queryParams, error to `any` for that endpoint.
type ApiSchema = {
users: {
$get: RequestInstance<{ response: User[]; endpoint: "/users" }>;
};
};
…that is a signal you wanted RequestModel instead. Inside a schema, omitted fields should mean "there is no payload
here" — not "anything goes". Switch the type and the rest of your code keeps the strictness it deserves:
import { RequestModel } from "@hyper-fetch/core";
// ✅ Omitted payload / queryParams / error stay strict (undefined / Error) instead of any.
type ApiSchema = {
users: {
$get: RequestModel<{ response: User[]; endpoint: "/users" }>;
};
};
See the SDK guide for the full schema pattern.
You've learned how to extend Hyper-Fetch requests for building flexible and type-safe components!
- You can create generic functions that constrain request types.
- You understand how to enforce a specific response shape on a request.
- You are able to build reusable components that are decoupled from specific API endpoints.
