$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

    View Slide

  2. leveraging types


    to make your UI rock solid
    algebraic data

    View Slide

  3. Matthias Le Brun


    @bloodyowl
    front-end dev @
    the easiest way to provide banking features


    (accounts, payments, cards…)
    we're hiring!

    View Slide

  4. but first, let's talk about


    quantum mechanics

    View Slide

  5. it's weird™

    View Slide

  6. nordic.physics

    View Slide

  7. «meh»
    — Erwin Schrödinger

    View Slide

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

    View Slide

  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

    View Slide

  10. what does this have to do
    with UI development?

    View Slide

  11. dealing with


    possible states

    View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. 1. presence


    2. success


    3. completion

    View Slide

  16. 1. presence


    2. success


    3. completion

    View Slide

  17. View Slide

  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

    View Slide

  19. type Day = {


    calories: number;


    workoutDuration: number;


    standUpHours: number;


    };


    type LastWeekActivity = Array;
    tracked
    untracked

    View Slide

  20. const getSarcasticMessage = (matchingDay: Day | undefined)
    =>

    matchingDay
    ===
    undefined


    ? "Your watch must look fantastic on its charger"


    : "This week must've been exhausting";

    View Slide

  21. lastWeekActivity


    .find(item
    =>

    item
    ===
    undefined
    ||

    item.workoutDuration
    ===
    0


    );
    untracked
    tracked

    View Slide

  22. interface Array {


    find(


    predicate: (value: T)
    =>
    boolean


    ): T | undefined;


    }

    View Slide

  23. interface Array {


    find(


    predicate: (value: Day | undefined)
    =>
    boolean


    ): Day | undefined | undefined;


    }

    View Slide

  24. interface Array {


    find(


    predicate: (value: Day | undefined)
    =>
    boolean


    ): Day | undefined | undefined;


    }

    View Slide

  25. null & undefined


    replace the value


    that's not there

    View Slide

  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

    View Slide

  27. View Slide

  28. View Slide

  29. type Option = Some | None;

    View Slide

  30. type LastWeekActivity = Array>>
    ;

    View Slide

  31. const find: (


    array: T[],


    func: (item: T)
    =>
    boolean


    )
    =>
    Option;

    View Slide

  32. const find: >>
    (


    array: Option[],


    func: (item: Option)
    =>
    boolean


    )
    =>
    Option>>
    ;

    View Slide

  33. Option>>

    View Slide

  34. option wraps the
    potential value

    View Slide

  35. None


    Some


    Some>>
    found nothing
    found untracked
    found tracked

    View Slide

  36. our code


    represents the domain,


    not the implementation
    details

    View Slide

  37. 1. presence


    2. success


    3. completion

    View Slide

  38. //
    BE CAREFUL
    !!!

    //
    The following function can throw


    function doStuff(input: string) {


    ...

    }

    View Slide

  39. ⚠ strong opinion ⚠

    View Slide

  40. exceptions are the worst
    way of dealing with
    errors

    View Slide

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

    View Slide

  42. type Result = Ok | Error;

    View Slide

  43. const f: ()
    =>
    Result;

    View Slide

  44. const parseInt: (value: string)
    =>

    Result<


    number,


    SyntaxError | IntOverflowError


    >;

    View Slide

  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

    View Slide

  46. explicit error handling,


    at type level

    View Slide

  47. 1. presence


    2. success


    3. completion

    View Slide

  48. View Slide

  49. type State = {


    isLoading: boolean;


    error?: Error;


    data?: Data;


    };

    View Slide







  50. {state.data?.jobs.length
    ??
    0}


    jobs in this workflow



    div>


    {state.isLoading
    &&
    />
    }



    div>
    wrong assumption


    allowed by the data structure

    View Slide

  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
    Done
    we don't want that
    legit

    View Slide

  52. type State = {


    isLoading: true | false;


    error: Error | undefined;


    data: Data | undefined;


    };
    2
    2
    2


    = 8

    View Slide

  53. type AsyncData =


    | NotAsked


    | Loading


    | Done;

    View Slide

  54. useEffect(()
    =>
    {


    setUser(AsyncData.Loading());


    const cancel = queryUser({ userId }, user
    =>
    {


    setUser(AsyncData.Done(user));


    });


    return cancel;


    }, [userId]);
    start loading
    receive


    a result

    View Slide

  55. type AsyncData =


    | NotAsked


    | Loading


    | Done;
    1
    1
    1
    +
    +
    = 3

    View Slide

  56. and that's the


    "algebraic" bit

    View Slide

  57. sum & product types

    View Slide

  58. possible state count:


    sum types: A + B + C


    product types: A * B * C

    View Slide

  59. type A = "One" | "Two";


    type B = [A, A];


    type C = { a: A; b: A };
    sum
    product

    View Slide

  60. type State = AsyncData>>
    ;
    types are composable

    View Slide

  61. type AsyncData =


    | NotAsked


    | Loading


    | Done>>

    | Done>>
    ;
    1
    1
    1
    +
    + = 4
    +
    1

    View Slide

  62. initial user
    error user

    View Slide

  63. {


    isLoading: boolean;


    data?: User;


    error?: number;


    };
    AsyncData<


    Result<


    Option,


    number


    >


    >;
    vs

    View Slide

  64. <>

    {user.isLoading
    ||

    (user.data
    ==
    null
    &&
    user.error
    ==
    null) ?


    null :


    user.error
    !=
    null ? (


    "An error occurred"


    ) : user.data
    !=
    null ? (


    />

    ) : (


    "No result was received"


    )}


    >
    ;
    never reached
    fig. 1: debugging the code 6 months from now

    View Slide

  65. initial user
    error user

    View Slide

  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)
    =>
    />
    )


    .exhaustive();

    View Slide

  67. github.com/bloodyowl/nordic2022
    want to play with it?

    View Slide

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


    → swan-io.github.io/boxed

    View Slide

  69. import {


    AsyncData,


    Option,


    Result,


    } from "@swan-io/boxed";

    View Slide

  70. const UserCard = ({user}: {user: AsyncData})
    =>
    {


    return user.match({


    NotAsked: ()
    =>
    null,


    Loading: ()
    =>
    `Loading`,


    Done: (user)
    =>
    {


    const name = user.name.getWithDefault("anonymous");


    return `Hello ${name}!`;


    },


    });


    };

    View Slide

  71. View Slide

  72. $ yarn add @swan-io/boxed

    View Slide

  73. avoid


    avoidable


    mistakes
    stets

    View Slide

  74. accidental complexity ↘


    business complexity ↗

    View Slide

  75. Matthias Le Brun


    @bloodyowl
    we're still hiring
    thank you! 🙏

    View Slide