Upgrade to Pro — share decks privately, control downloads, hide ads and more …

leveraging (algebraic data) types to make your UI rock @ jsheroes

leveraging (algebraic data) types to make your UI rock @ jsheroes

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

May 19, 2023
Tweet

More Decks by Matthias Le Brun

Other Decks in Technology

Transcript

  1. Matthias Le Brun @bloodyowl lead front-end manager @ banking API

    as a service (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. Array .find( ... ) .map(getSarcasticMessage); const index = last30daysActivity .findIndex(

    ... ); const message = index > -1 ? getSarcasticMessage( last30daysActivity[index] ) : undefined;
  10. // BE CAREFUL !!! // The following function can throw

    function doStuff(input: string) { ... }
  11. 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
  12. <div> <div> {state.data?.jobs.length ?? 0} jobs in this workflow <

    / div> {state.isLoading && <Loader /> } </ div> wrong assumption allowed by the data structure
  13. 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
  14. type State = { isLoading: true | false; error: Error

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

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

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

    | Done<Error<E >> ; 1 1 1 + + = 4 + 1
  18. <> {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
  19. 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();
  20. acc as Result< { [K in keyof Results]: Results[K] extends

    Result<infer T, any> ? T : never; }, { [K in keyof Results]: Results[K] extends Result<any, infer T> ? T : never; }[number] >;
  21. const UserCard = ({user}: {user: AsyncData<User>}) => { return user.match({

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