A TypeScript utility for handling responses with type safety and readable error handling.
This utility snippet is particularly useful to handle responses from API calls, server actions, or any other operations that can return success or failure. It has several benefits:
Copy and paste the following code into your project.
export type HandledResponse<D, E = D> =
| {
readonly data: D;
readonly error: never;
}
| {
readonly data: never;
readonly error: E;
};
export const ok = <D, E = D>(data: D): HandledResponse<D, E> => ({
data,
error: undefined as never,
});
export const err = <D, E = D>(error: E): HandledResponse<D, E> => ({
data: undefined as never,
error,
});import { HandledResponse, ok, err } from "@/lib/handled-response";import { HandledResponse, ok, err } from "@/lib/handled-response";
// Imagine this is your API call or database query
async function getPostMessageCrud(postId: string): Promise<HandledResponse<string>> {
try {
const post = await db.findById(postId);
if (!post) {
return err("Post not found"); // Automatically narrows the type to { error: string }
}
return ok(post.message); // Automatically narrows the type to { data: string }
}
}Then
async function getPostMessage(postId: string) {
const { error, data } = await getPostMessageCrud(postId);
data?.name; // Error: data is optional, so it might not exist
if (error) {
// Type narrowed: error is string
console.error("Failed to fetch user:", error);
return;
}
// Type narrowed: data is string
console.log("Post message found:", data);
}Suppose you have a function that fetches user data from a database or an API. You want to strictly handle the possible outcomes (success or error) with type safety.
import { HandledResponse, ok, err } from "@/lib/handled-response";
// Imagine this is your API call or database query
async function fetchUserById(userId: string): Promise<HandledResponse<User,string>> {
try {
const user = await db.findUser(userId);
if (!user) {
return err("User not found"); // Automatically narrows the type to { error: string }
}
return ok(user); // Automatically narrows the type to { data: User }
} catch (error) {
return err("Unexpected error");
}
}You can then use the returned object in an exhaustive way and typeScript will enforce that you check both possible states:
async function handleUserRequest(userId: string) {
const { error, data } = await fetchUserById(userId);
data?.name; // Error: data is optional, so it might not exist
if (error) {
// Type narrowed: error is string
console.error("Failed to fetch user:", error);
return;
}
// Type narrowed: data is User
console.log("User found:", data.name, data.email);
}