-
Notifications
You must be signed in to change notification settings - Fork 2
/
fetching-data.ts
121 lines (92 loc) · 4.67 KB
/
fetching-data.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// ! NOTE: You need to clone this repository locally and open it in your IDE (VS Code) to see typesafety in action.
import { exception, isExceptionallyResult, success } from 'exceptionally'
import { assertSuccess, guardSuccess } from 'exceptionally/assert'
type Json = JsonArray | JsonObject | JsonPrimitive
type JsonArray = Json[]
type JsonObject = {
[K in string]?: Json
}
type JsonPrimitive = boolean | null | number | string
// --------------------------------------------------------------------------------------------------------------------
// define different Exception classes for all kind of exceptions
// NOTE: somehow TypeScript can't distinguish between different Classes that derive from `Error`.
// As a workaround we can set a property inside that class to make inference work again.
class NetworkException extends Error {
readonly #id = Symbol('NetworkException')
}
class DecodeJsonException extends Error {
readonly #id = Symbol('DecodeJsonException')
}
class HttpException extends Error {
readonly #id = Symbol('HttpException')
}
class EmptyDatasetException extends Error {
readonly #id = Symbol('EmptyDatasetException')
}
// when fetching data, a few things can happen.. when something fails, it is good to know what has triggered the issue
const fetchData = async <ReturnType extends Json>(endpoint: string) => {
const fetchResult = await fetch(endpoint)
.catch(e => exception(new NetworkException(e))) // the server could be unavailable ...
if (isExceptionallyResult(fetchResult)) return fetchResult // pass forward exception
// ... the request could fail with a statuscode 4xx or 5xx ...
if (!fetchResult.ok) return exception(new HttpException(`${fetchResult.status}: ${fetchResult.statusText}`))
const dataResult = await fetchResult.json()
.then(data => data as ReturnType)
.catch(e => exception(new DecodeJsonException(e))) // ... or the payload could consist of invalid JSON ...
if (isExceptionallyResult(dataResult)) return dataResult // pass forward exception
if (Array.isArray(dataResult) && !dataResult.length) {
return exception(new EmptyDatasetException()) // ... or maybe the validation of the data could fail ..
}
return success(dataResult) // ... and finally fetching data could also be successful
}
// --------------------------------------------------------------------------------------------------------------------
type User = {
id: string
name: string
}
const getUsers = async () => {
const fetchUsersResult = await fetchData<User[]>('https://some-api.com/users')
// whenever we get back a result, we should check for exceptions first
if (fetchUsersResult.isException) {
const exc = fetchUsersResult()
// handle a certain type of exception individually
if (exc instanceof NetworkException) {
return exception('Could not fetch users. Please try again later.')
}
// we don't really care about the `EmptyDatasetException` here, so we return an empty array
if (exc instanceof EmptyDatasetException) {
return success([])
}
// in all other cases, we return a general error message
return exception('Unexpected error. Please contact the support-team.')
}
// the result can either be successful and contain our users or be of type `EmptyDatasetException`
const users = fetchUsersResult()
// do something with the data
console.info(`successfully fetched ${users.length} users`)
return success(users) // pass forward data
}
// --------------------------------------------------------------------------------------------------------------------
const getAllUsernames = async () => {
const usersResult = await getUsers()
// even after multiple nested function calls we can make sure what all possible outcomes can be
if (usersResult.isException) {
// at the end of our program flow we finally throw the error that e.g. get's displayed to the user
throw new Error(usersResult())
}
// we can make sure that we have catch'ed all exceptions
// TypeScript will let you know if you forget to handle an exception
// try to remove the line that throws the Error above and you'll see an error here
guardSuccess(usersResult) // only type-safety
// if you don't trust TypeScript ton warn you about unhandled exceptions, you can use the following line
// when this line get's executed on runtime, it will throw an error
assertSuccess(usersResult) // type-safety with additional runtime-safety
// at the end we return the data without wrapping it, so it can be consumed in an usual way
return usersResult().map(({ name }) => name)
}
// --------------------------------------------------------------------------------------------------------------------
const run = async () => {
const usernames = await getAllUsernames()
console.info(usernames)
}
run()