29
loading...
This website collects cookies to deliver better user experience
interface User {
id: number;
name: string;
bio: string;
}
// this is allowed in TypeScript
const newUser: User = {id: null, name: null, bio: null};
let userBio: string;
if(newUser != null) {
if(newUser.id != null) {
if(newUser.bio != null) {
userBio = newUser.bio;
} else {
userBio = "N/A";
}
} else {
useBio = "N/A"
}
} else {
userBio = "N/A"
}
// this code may blow up since not only may the fetch operation
// fail but it could succeed and return a null newUsers array
const newUsers: User[] = await fetchUsersFromAPI();
for (const newUser of newUsers) {
if(newUser.bio != null) {
uploadUserBio(newUser.bio);
}
}
type Option<A> =
| { type: 'None' } // our operation failed
| { type: 'Some'; value: A } // our operation succeeded and we have a value of type A
// construct for a null or none type
const none: Option<never> = { type: 'None' }
//constructor for a value that actually exists
const some = <A>(value: A): Option<A> => ({ type: 'Some', value })
//an operation to 'match' an expression
const fold = <A, R>(fa: Option<A>, onNone: () => R, onSome: (a: A) => R): R =>
fa.type === 'None' ? onNone() : onSome(fa.value)
Option
forces us to deal with the case in which the value doesn't exist or is not what we want.// if our API had the following signature
// (we'll learn later about how to implement this)
declare fetchUsersFromAPI: () => Promise<Option<User[]>>
// then we're forced to 'unwrap' the Option and deal with the
// case in which in may be null or an error
const newUsers: Option<User[]> = await fetchUsersFromAPI();
fold(
newUsers,
() => 'There are no users!',
(users) => {
for (const newUser of users) {
if(newUser.bio != null) {
uploadUserBio(newUser.bio);
}
}
})
newUsers
array will be forced to deal with the fact that newUsers
may not exist.type Either<L, A> =
| { type: 'Left'; left: L } // holding a failure
| { type: 'Right'; right: A } // holding a success
const fold = <L, A, R>(
fa: Either<L, A>,
onLeft: (left: L) => R,
onRight: (right: A) => R
): R => (fa.type === 'Left' ? onLeft(fa.left) : onRight(fa.right))
Either
type is similar to the Option
type but it can hold more information about why our operation failed if it indeed does. Usually the Left
value holds an error while the Right
value holds a successfully retrieved value.Either
// the new function signiture returning an Either
declare fetchUsersFromAPI: () => Promise<Either<string, User[]>>
const newUsers: Either<string, User[]> = await fetchUsersFromAPI();
fold(
newUsers,
(m: string) => `Something went wrong ${m}!`,
(users) => {
for (const newUser of users) {
if(newUser.bio != null) {
uploadUserBio(newUser.bio);
}
}
})
string
containing a message as our Left
value in the event of an error. We could choose to implement this in anyway we want however. We could return an Error
instance containing more detailed information about what went wrong. Regardless, as with the Option
anyone who wants to use the newUsers object will need to account for the possibility that the fetch operation failed.Options
and Eithers
when a simple if
check could suffice. The point so far however is to understand that we have much to gain by treating certain classes of data as existing in a dual state (like a superposition in physics). Instead of pretending that a piece of data fetched from a remote API will always represent the data we want (oh if it were so) we can use abstractions like Option
and Either
such that the possibility that they don't is considered as a matter of course. This will make our code not only more robust, but also easier to read and more fun to write.Options
, Eithers
and other FP abstractions we haven't explored yet.