Skip to content

Instantly share code, notes, and snippets.

@sneas
Last active November 19, 2025 07:39
Show Gist options
  • Select an option

  • Save sneas/de2144be04a7b9c59f065d76d25c9a45 to your computer and use it in GitHub Desktop.

Select an option

Save sneas/de2144be04a7b9c59f065d76d25c9a45 to your computer and use it in GitHub Desktop.
TS Result type
// First, let's define the generic result type
type Success<T> = {
success: true;
value: T;
};
// Pay attention to the fact that
// Error is not a generic type
// we will use it for our advantage
type Error = {
success: false;
reason: string;
};
export type Result<T> = Success<T> | Error;
// After the result type is defined, let's create a function that wraps a promise with a result type
const resultify = async <T>(p: Promise<T>): Promise<Result<T>> => {
try {
const value = await p;
return { success: true, value };
} catch (e: unknown) {
return {
success: false,
reason: e.toString(),
};
}
};
// Now let's use resultify to fetch some JSON Data
const fetchJsonObject = (
...params: Parameters<typeof fetch>
): Promise<Result<unknown>> => {
return resultify(
fetch(...params).then((response) => {
if (!response.ok) {
throw 'Unable to fetch';
}
return response.json();
})
);
};
// The fetchJsonObject (defined above) NEVER throws an exception or rejects.
// Instead, it returns either {success: true, value: Object} or {success: false}
// When user calls fetchJsonObject, TypeScript will enforce users
// to take care about possible {success: false} - this is how
// errors become the explicit part of application data model.
// Let's use the above code for some practical purpose
type User = {
name: string;
email: string;
};
const isUser = (data: any): data is User => {
return typeof data.name === 'string' && typeof data.age === 'string';
};
const fetchUser = async (): Promise<Result<User>> => {
const result = await fetchJsonObject(
'https://jsonplaceholder.typicode.com/users/1'
);
// Every time we call a function that returns a Result type,
// we need to take care of possible Error return.
// result.value does not exist for developers
// until they make sure that result is successful.
// Here is how they do it:
if (!result.success) {
// In his branch of code
// we know that the result is unsuccessful and non-generic (Error type)
// so we can just pass it to the caller with return.
return result;
}
// After returning the error, we may be confident that
// result.value exists
// But let's make sure that result.value is of the expected type:
if (!isUser(result.value)) {
return { success: false, reason: 'Malformed user data' };
}
// After validating that the result is successful and
// of a necessary type, we can safely use it for our purposes.
// At this point, we can be sure that
// the result is what we expect;
return result;
};
//... and a todo item
type ToDoItem = {
id: number;
title: string;
completed: boolean;
};
const isToDoItem = (data: any): data is ToDoItem => {
return typeof data.id === 'number' && typeof data.title === 'string';
};
const fetchToDoItems = async (): Promise<Result<ToDoItem[]>> => {
const result = await fetchJsonObject(
'https://jsonplaceholder.typicode.com/todos/1'
);
if (!result.success) {
return result;
}
if (!Array.isArray(result.value) || !result.value.every(isToDoItem)) {
return {
success: false,
reason: 'Malformed ToDo items',
};
}
return result;
};
// Now let's bake it all together
const fetchAll = async (): Promise<
Result<{ user: User; toDoItems: ToDoItem[] }>
> => {
// We don't need to wrap this block with try/catch,
// because both fetchUser and fetchToDoItems always resolves
// as a result type.
const [userResult, itemsResult] = await Promise.all([
fetchUser(),
fetchToDoItems(),
]);
// Without those two IFs the code won't compile:
if (!userResult.success) {
// We can return non-successful userResult
// because it is not of generic Error type
return userResult;
}
if (!itemsResult.success) {
// We can return non-successful itemsResult
// because it is not of generic Error type
return itemsResult;
}
// After the IFs above we can be sure that
// both userResult and itemsResult are fine.
return {
success: true,
value: {
user: userResult.value,
toDoItems: itemsResult.value,
},
};
};
// The beauty of ResultType is that if
// a function can return Success or Error,
// TypeScript forces developers to validate it
// explicitly. Developers can't "forget" about
// the erroneous outcome, as often happens with
// exceptions and try/catch blocks.
// If, at the beginning of project, developer makes sure that all the exceptions
// in the system are replaced with ResultType, they get very predictable code
// because TypeScript will keep nagging developers to explicitly
// take care of all the errors and won't compile if they don't.
// Instead of having functions that can throw exceptions
// we will have functions that return a ResultType.
// So if at some point you need to call fetchAll() in some other function,
// your caller will also have to resolve into result type:
const moreComplexFunction = async (): Promise<Result<unknown>> => {
const result = await fetchAll();
if (!result.success) {
return result;
}
// After the above IF, we can be sure that our result
// is fine, and we can use it to our advantage.
return sendToDos(result.value.user.email, result.value.toDoItems);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment