Last active
November 19, 2025 07:39
-
-
Save sneas/de2144be04a7b9c59f065d76d25c9a45 to your computer and use it in GitHub Desktop.
TS Result type
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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