Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Leveraging (algebraic data) types to make your ...

Leveraging (algebraic data) types to make your UI rock solid

As we always want to push the envelope and manage more and more on the front-end, our UI code code grew in complexity in the last few years. All of our modern application deal with three big challenges: optionality (is my data there or not?), fallibility (did my operation succeed?) and asynchronicity (when do I have my data?). Inspired by how typed functional languages treat those, we'll see how we can leverage TypeScript to deal with them and make a whole category of issues disappear, so that we can focus on the essential parts: making great, accessible interfaces.

Matthias Le Brun

October 07, 2022
Tweet

More Decks by Matthias Le Brun

Other Decks in Technology

Transcript

  1. Matthias Le Brun @bloodyowl front-end dev @ the easiest way

    to provide banking features (accounts, payments, cards…) we're hiring!
  2. quantum event → poison → dead dude i swear to

    god, you put me in that box again and I quit no quantum event → no poison → no dead
  3. «let's add a snarky banner to remind the user to

    wear their watch and do some workout if they missed any day on the previous week» — the product team
  4. type Day = { calories: number; workoutDuration: number; standUpHours: number;

    }; type LastWeekActivity = Array<Day | undefined>; tracked untracked
  5. const getSarcasticMessage = (matchingDay: Day | undefined) => matchingDay ===

    undefined ? "Your watch must look fantastic on its charger" : "This week must've been exhausting";
  6. interface Array<Day | undefined> { find( predicate: (value: Day |

    undefined) => boolean ): Day | undefined | undefined; }
  7. interface Array<Day | undefined> { find( predicate: (value: Day |

    undefined) => boolean ): Day | undefined | undefined; }
  8. const index = last30daysActivity.findIndex(item => item == undefined || item.calories

    < 50 ); if (index == -1) { // not found } else { const item = last30daysActivity[index]; if (item == undefined) { // found day without recorded activity } else { // found day with low activity } } the correct way leaks implementation details
  9. // BE CAREFUL !!! // The following function can throw

    function doStuff(input: string) { ... }
  10. match(parsed) .with(Ok(P.select()), (value) => doSomething(value)) .with(Error(P.instanceOf(SyntaxError)), () => showToast("Invalid input")

    ) .with(Error(P.instanceOf(IntOverflowError)), () => showToast("Number is too big") ) .exhaustive(); handles exhaustivity just a value
  11. <div> <div> {state.data?.jobs.length ?? 0} jobs in this workflow </

    div> {state.isLoading && <Loader /> } </ div> wrong assumption allowed by the data structure
  12. isLoading error data FALSE NULL NULL TRUE NULL NULL FALSE

    ERROR NULL FALSE NULL DATA TRUE TRUE NULL TRUE FALSE DATA FALSE TRUE DATA TRUE TRUE DATA NotAsked Loading Done<Ok> Done<Error> we don't want that legit
  13. type State = { isLoading: true | false; error: Error

    | undefined; data: Data | undefined; }; 2 2 2 ⨉ ⨉ = 8
  14. useEffect(() => { setUser(AsyncData.Loading()); const cancel = queryUser({ userId },

    user => { setUser(AsyncData.Done(user)); }); return cancel; }, [userId]); start loading receive a result
  15. type A = "One" | "Two"; type B = [A,

    A]; type C = { a: A; b: A }; sum product
  16. type AsyncData<A> = | NotAsked | Loading | Done<Ok<A >>

    | Done<Error<E >> ; 1 1 1 + + = 4 + 1
  17. <> {user.isLoading || (user.data == null && user.error == null)

    ? null : user.error != null ? ( "An error occurred" ) : user.data != null ? ( <UserCard user={user.data} /> ) : ( "No result was received" )} </> ; never reached fig. 1: debugging the code 6 months from now
  18. match(user) .with(NotAsked, Loading, () => null) .with(Done(Error(P.any)), () => "An

    error occurred") .with(Done(Ok(None)), () => "No result was received") .with(Done(Ok(Some(P.select()))), (user) => <UserCard user={user} /> ) .exhaustive();
  19. const UserCard = ({user}: {user: AsyncData<User>}) => { return user.match({

    NotAsked: () => null, Loading: () => `Loading`, Done: (user) => { const name = user.name.getWithDefault("anonymous"); return `Hello ${name}!`; }, }); };