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. 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
    lead front-end manager @
    banking API as a service


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

    View Slide

  4. but first, let's
    talk about


    quantum
    physics

    View Slide

  5. it's weird™

    View Slide

  6. physicists
    when this kind
    of stuff was
    observed

    View Slide

  7. PhysicsHeroes

    View Slide

  8. «meh»
    — Erwin Schrödinger
    (source: Wikipedia)

    View Slide

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

    View Slide

  10. 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

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

    View Slide

  12. boxes
    will make sense
    in a few slides

    View Slide

  13. dealing with


    possible states

    View Slide

  14. View Slide

  15. View Slide

  16. View Slide

  17. 3 kind of states


    in almost every app

    View Slide

  18. 1. presence


    2. success


    3. completion

    View Slide

  19. 1. presence


    2. success


    3. completion

    View Slide

  20. View Slide

  21. «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

  22. type Day = {


    calories: number;


    workoutDuration: number;


    standUpHours: number;


    };


    type LastWeekActivity = Array;
    tracked
    untracked

    View Slide

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

    matchingDay
    ===
    undefined


    ? "Your watch must look fantastic on its charger"


    : "This week must've been exhausting";

    View Slide

  24. lastWeekActivity


    .find(item
    =>

    item
    ===
    undefined
    ||

    item.workoutDuration
    ===
    0


    );
    untracked
    tracked

    View Slide

  25. interface Array {


    find(


    predicate: (value: T)
    =>
    boolean


    ): T | undefined;


    }

    View Slide

  26. interface Array {


    find(


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


    ): Day | undefined | undefined;


    }

    View Slide

  27. interface Array {


    find(


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


    ): Day | undefined | undefined;


    }

    View Slide

  28. null & undefined


    replace the value


    that's not there

    View Slide

  29. 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

  30. View Slide

  31. View Slide

  32. type Option = Some | None;

    View Slide

  33. type LastWeekActivity = Array>>
    ;

    View Slide

  34. Array.find(lastWeekActivity, (item)
    =>

    match(item)


    .with(None, ()
    =>
    true)


    .with(Some({ workoutDuration: 0 }), ()
    =>
    true)


    .otherwise(()
    =>
    false)


    );

    View Slide

  35. const find: (


    array: T[],


    func: (item: T)
    =>
    boolean


    )
    =>
    Option;

    View Slide

  36. const find: >>
    (


    array: Option[],


    func: (item: Option)
    =>
    boolean


    )
    =>
    Option>>
    ;

    View Slide

  37. Option>>

    View Slide

  38. option wraps the
    potential value

    View Slide

  39. None


    Some


    Some>>
    found nothing
    found untracked
    found tracked

    View Slide

  40. Array


    .find(
    ...
    )


    .map(getSarcasticMessage);
    const index =


    last30daysActivity


    .findIndex(
    ...
    );


    const message =


    index > -1


    ? getSarcasticMessage(


    last30daysActivity[index]


    )


    : undefined;


    View Slide

  41. our code


    represents the domain,


    not the implementation
    details

    View Slide

  42. 1. presence


    2. success


    3. completion

    View Slide

  43. //
    BE CAREFUL
    !!!

    //
    The following function can throw


    function doStuff(input: string) {


    ...

    }

    View Slide

  44. getGreetingMessage
    getCurrentUser
    calls
    getUserById
    calls returns
    returns

    View Slide

  45. throws
    getGreetingMessage
    getCurrentUser
    calls
    getUserById
    calls
    getUserById

    View Slide

  46. throws
    getGreetingMessage
    getCurrentUser
    calls
    getUserById
    calls
    getUserById

    View Slide

  47. exceptions are
    the worst way of
    dealing with
    errors
    in JS at least

    View Slide

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

    View Slide

  49. throw 1;


    throw { id: "foo" };


    throw fetch("/api");
    ¯\_(ツ)_/¯

    View Slide

  50. try {


    something()


    } catch(err) {


    //
    `err` is any


    }
    can't type this

    View Slide

  51. Promise;


    promise.catch(err
    =>
    {


    //
    `err` is any


    })
    can't type
    rejection type where 😢

    View Slide

  52. type Result = Ok | Error;

    View Slide

  53. const f: ()
    =>
    Result;

    View Slide

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

    Result<


    number,


    SyntaxError | IntOverflowError


    >;

    View Slide

  55. 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

  56. explicit error handling,


    at type level

    View Slide

  57. 1. presence


    2. success


    3. completion

    View Slide

  58. View Slide

  59. type State = {


    isLoading: boolean;


    error?: Error;


    data?: Data;


    };

    View Slide







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


    jobs in this workflow


    <
    /
    div>


    {state.isLoading
    &&
    />
    }



    div>
    wrong assumption


    allowed by the data structure

    View Slide

  61. 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

  62. type State = {


    isLoading: true | false;


    error: Error | undefined;


    data: Data | undefined;


    };
    2
    2
    2


    = 8

    View Slide

  63. type AsyncData =


    | NotAsked


    | Loading


    | Done;

    View Slide

  64. useEffect(()
    =>
    {


    setUser(AsyncData.Loading());


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


    setUser(AsyncData.Done(user));


    });


    return cancel;


    }, [userId]);
    start loading
    receive


    a result

    View Slide

  65. type AsyncData =


    | NotAsked


    | Loading


    | Done;
    1
    1
    1
    +
    +
    = 3

    View Slide

  66. and that's the


    "algebraic" bit

    View Slide

  67. sum & product types

    View Slide

  68. possible state count:


    sum types: A + B + C


    product types: A * B * C

    View Slide

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


    type B = [A, A];


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

    View Slide

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

    View Slide

  71. type AsyncData =


    | NotAsked


    | Loading


    | Done>>

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

    View Slide

  72. initial user
    error user

    View Slide

  73. {


    isLoading: boolean;


    data?: User;


    error?: number;


    };
    AsyncData<


    Result<


    Option,


    number


    >


    >;
    vs

    View Slide

  74. <>

    {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

  75. initial user
    error user

    View Slide

  76. 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

  77. github.com/bloodyowl/jsheroes2023
    want to play with it?

    View Slide

  78. how can I have this


    in TypeScript?
    (or plain JS with a worse DX?)

    View Slide

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


    → swan-io.github.io/boxed
    how convenient!

    View Slide

  80. import {


    AsyncData,


    Option,


    Result,


    } from "@swan-io/boxed";

    View Slide

  81. import {


    AsyncData,


    Option,


    Result,


    } from "@swan-io/boxed";

    View Slide

  82. acc as Result<


    {


    [K in keyof Results]: Results[K] extends Result


    ? T


    : never;


    },


    {


    [K in keyof Results]: Results[K] extends Result


    ? T


    : never;


    }[number]


    >;

    View Slide

  83. 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

  84. View Slide

  85. $ yarn add @swan-io/boxed

    View Slide

  86. avoid


    avoidable


    mistakes
    stets

    View Slide

  87. accidental complexity ↘


    business complexity ↗

    View Slide

  88. Matthias Le Brun


    @bloodyowl
    we're still hiring
    thank you! 🙏

    View Slide