$30 off During Our Annual Pro Sale. View Details »

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

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. leveraging types to make your UI rock solid algebraic data

  2. leveraging types to make your UI rock solid algebraic data

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

    to provide banking features (accounts, payments, cards…) we're hiring!
  4. but first, let's talk about quantum mechanics

  5. it's weird™

  6. nordic.physics

  7. «meh» — Erwin Schrödinger

  8. Schrödinger's cat* * artist's impression

  9. 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
  10. what does this have to do with UI development?

  11. dealing with possible states

  12. None
  13. None
  14. None
  15. 1. presence 2. success 3. completion

  16. 1. presence 2. success 3. completion

  17. None
  18. «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
  19. type Day = { calories: number; workoutDuration: number; standUpHours: number;

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

    undefined ? "Your watch must look fantastic on its charger" : "This week must've been exhausting";
  21. lastWeekActivity .find(item => item === undefined || item.workoutDuration === 0

    ); untracked tracked
  22. interface Array<T> { find( predicate: (value: T) => boolean ):

    T | undefined; }
  23. interface Array<Day | undefined> { find( predicate: (value: Day |

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

    undefined) => boolean ): Day | undefined | undefined; }
  25. null & undefined replace the value that's not there

  26. 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
  27. None
  28. None
  29. type Option<A> = Some<A> | None;

  30. type LastWeekActivity = Array<Option<Day >> ;

  31. const find: <T>( array: T[], func: (item: T) => boolean

    ) => Option<T>;
  32. const find: <Option<Day >> ( array: Option<Day>[], func: (item: Option<Day>)

    => boolean ) => Option<Option<Day >> ;
  33. Option<Option<Day >>

  34. option wraps the potential value

  35. None Some<None> Some<Some<Day >> found nothing found untracked found tracked

  36. our code represents the domain, not the implementation details

  37. 1. presence 2. success 3. completion

  38. // BE CAREFUL !!! // The following function can throw

    function doStuff(input: string) { ... }
  39. ⚠ strong opinion ⚠

  40. exceptions are the worst way of dealing with errors

  41. you don't know where/if an exception is handled in the

    call stack
  42. type Result<A, E> = Ok<A> | Error<E>;

  43. const f: () => Result<Ok, Error>;

  44. const parseInt: (value: string) => Result< number, SyntaxError | IntOverflowError

    >;
  45. 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
  46. explicit error handling, at type level

  47. 1. presence 2. success 3. completion

  48. None
  49. type State = { isLoading: boolean; error?: Error; data?: Data;

    };
  50. <div> <div> {state.data?.jobs.length ?? 0} jobs in this workflow </

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

    | undefined; data: Data | undefined; }; 2 2 2 ⨉ ⨉ = 8
  53. type AsyncData<A> = | NotAsked | Loading | Done<A>;

  54. useEffect(() => { setUser(AsyncData.Loading()); const cancel = queryUser({ userId },

    user => { setUser(AsyncData.Done(user)); }); return cancel; }, [userId]); start loading receive a result
  55. type AsyncData<A> = | NotAsked | Loading | Done<A>; 1

    1 1 + + = 3
  56. and that's the "algebraic" bit

  57. sum & product types

  58. possible state count: sum types: A + B + C

    product types: A * B * C
  59. type A = "One" | "Two"; type B = [A,

    A]; type C = { a: A; b: A }; sum product
  60. type State = AsyncData<Result<Ok, Error >> ; types are composable

  61. type AsyncData<A> = | NotAsked | Loading | Done<Ok<A >>

    | Done<Error<E >> ; 1 1 1 + + = 4 + 1
  62. initial user error user

  63. { isLoading: boolean; data?: User; error?: number; }; AsyncData< Result<

    Option<User>, number > >; vs
  64. <> {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
  65. initial user error user

  66. 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();
  67. github.com/bloodyowl/nordic2022 want to play with it?

  68. Boxed → github.com/swan-io/boxed → swan-io.github.io/boxed

  69. import { AsyncData, Option, Result, } from "@swan-io/boxed";

  70. const UserCard = ({user}: {user: AsyncData<User>}) => { return user.match({

    NotAsked: () => null, Loading: () => `Loading`, Done: (user) => { const name = user.name.getWithDefault("anonymous"); return `Hello ${name}!`; }, }); };
  71. None
  72. $ yarn add @swan-io/boxed

  73. avoid avoidable mistakes stets

  74. accidental complexity ↘ business complexity ↗

  75. Matthias Le Brun @bloodyowl we're still hiring thank you! 🙏