Dealing with things that may or may not be there.
- Here is a horse type
type Horse = {
type: 'HORSE'
name: string
legs: number
hasTail: boolean
}
- Here are some horses
const goodHorses: Horse[] = [
{
type: 'HORSE',
name: 'CHAMPION',
legs: 3,
hasTail: false,
},
{
type: 'HORSE',
name: 'HOOVES_GALORE',
legs: 4,
hasTail: true,
},
]
- And a function for trying to find a horse by name
const getHorse = (name: string) => {
let found
goodHorses.forEach(goodHorse => {
if (goodHorse.name === name) {
found = goodHorse
}
})
return found
}
- What if there isn't a match?
const horse = getHorse('CHAMPION')
if (horse) {
// do stuff with horse
}
- Seems fine I guess
- Say we now need to tidy up those weird uppercase names
const tidyHorseName = (horse: Horse): Horse => {
return {
...horse,
name: horse.name.toLowerCase(),
}
}
- But wait, we're not dealing with
Horse
-
We're dealing with
Horse | undefined
-
So we either make our horse tidying function more accomodating...
const tidyHorseName = (
horse: Horse | undefined
): Horse | undefined => {
if (!horse) {
return undefined
}
return {
...horse,
name: horse.name.toLowerCase(),
}
}
- Or we are more careful about when we use it
const horse = getHorse('CHAMPION')
const tidyHorse = horse ? tidyHorseName(horse) : undefined
-
Rules are rules, we are going to need to inspect this horse for a few things
-
We are going to make a new type that describes the good horse
type StandardHorse = {
name: string
hasTail: true
legs: 4
type: 'STANDARD_HORSE'
}
-
That way, we can use types to make sure we don't pass a non-standard horse where it's not wanted
-
Here's our check:
const mandatoryTailCheck = (
horse: Horse
): StandardHorse | undefined => {
if (!horse.hasTail || horse.legs !== 4) {
return undefined
}
return {
name: horse.name,
hasTail: true,
legs: 4,
type: 'GOOD_HORSE',
}
}
-
Once again, we have two choices for dealing with the potential lack of
horse
-
We put the burden on the function itself:
const mandatoryTailCheck = (
horse: Horse | undefined
): GoodHorse | undefined => {
if (!horse || !horse.hasTail || horse.legs !== 4) {
return undefined
}
return {
name: horse.name,
hasTail: true,
legs: 4,
type: 'GOOD_HORSE',
}
}
- Or we put the burden on the caller of the function:
const horse = getHorse('CHAMPION')
const tidyHorse = horse ? tidyHorseName(horse) : undefined
const goodHorse = tidyHorse
? mandatoryTailCheck(tidyHorse)
: undefined
-
.
-
..
-
...
-
If you chose none of them, i want more abstraction then you are correct
- We're familiar with union types right?
type Fuel = string | null | number
- A discriminated union is a union where there is some sort of unique key
// like Redux actions, innit
type Smash = {
type: 'SMASH_THAT_LIKE_BUTTON'
timestamp: number
}
type GiveUp = {
type: 'GIVE_UP'
}
type Action = Smash | GiveUp
-
(said unique key is the discriminator, surprise)
-
There is no magic here, it just means we can easily switch on it
-
Like a Redux reducer
const weirdReducer = (action: Action) {
switch (action.type) {
case 'SMASH_THAT_LIKE_BUTTON':
// Typescript knows timestamp should be here
return action.timestamp
case 'GIVE_UP':
// Typescript knows timestamp will not be here
return 0
}
}
Option is a container for holding things that may or may not be there:
type Option<A> = { type: 'Some'; value: A } | { type: 'None' }
-
Because it uses a generic parameter, we can make a
Option<string>
or aOption<number>
, depending on what it (maybe) contains. -
We can make some nice helpers for these:
some :: a -> Option a
const some = <A>(value: A): Option<A> => ({ type: 'Some', value })
const a = some('horses')
// a == { type: "Some", value: "horses" }
- and...
none :: () -> option never
const none = (): Option<never> => ({ type: 'None' })
const b = none()
// b == { type: "None" }
- We can then return this where we would have partial data
getHorse :: string -> Option Horse
const getHorse = (name: string): Option<Horse> => {
const found = goodHorses.find(goodHorse => goodHorse.name === name)
return found ? some(found) : none()
}
- Example 1
getHorse("CHAMPION")
/*
{ type: "Some",
value:{
type: "HORSE",
name: "CHAMPION",
legs: 3,
hasTail: false,
}
}
- Example 2
getHorse("NON-EXISTANT-HORSE")
/*
{ type: "None" }
/*
We can use a function called map
:
// map :: (A -> B) -> Option A -> Option B
const map = (f: (a: A) => B, option: Option<A>): Option<B> =>
option.type === 'Some' ? some(f(option.value)) : none()
Think of it working like Array.map
- if the Array
is empty, nothing
happens, and if there's items inside, we run the function on it.
- Down to you...