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

Hello generics, goodbye boilerplate (WAD live)

Hello generics, goodbye boilerplate (WAD live)

Do you have a feeling that adding a new feature in your enterprise application is more chore than it is a mental challenge? Copy-paste components, services, actions, reducer, effects... You are not the only one. All big applications eventually suffer from the same symptom - huge amounts of boilerplate code.

Luckily, TypeScript provides a solution! By smartly using generics we can get rid of this boilerplate.

Come join me on this quest of removing the clutter once and for all and bringing out the essential parts into focus!

Miroslav Jonaš

December 01, 2020
Tweet

More Decks by Miroslav Jonaš

Other Decks in Programming

Transcript

  1. Hello generics,
    goodbye boilerplate
    @meeroslav

    View Slide

  2. developers
    love to
    code
    @meeroslav

    View Slide

  3. developers
    love to
    copy/paste
    code
    @meeroslav

    View Slide

  4. developers
    love to
    copy/paste boilerplate
    code
    @meeroslav

    View Slide

  5. Johnson
    vs
    Boilerplate
    @meeroslav

    View Slide

  6. … sections of code that
    have to be included in
    many places with
    little or no alteration …
    @meeroslav

    View Slide

  7. 90%
    @meeroslav

    View Slide

  8. Miroslav Jonaš
    @meeroslav

    View Slide

  9. First there was enterprise...
    @meeroslav

    View Slide

  10. Then came the state machine
    @meeroslav

    View Slide

  11. View Reducer
    Action
    Store
    Effect
    The Redux flow
    @meeroslav

    View Slide

  12. LIVE TUESDAY DECEMBER 1 16:15 on WDLW
    Most Revealing Trailer
    Noscar Nominations 2020
    Most Unsuitable Score
    Noscar Nominations 2020
    Most Predictable Plot
    Noscar Nominations 2020
    Most Rip-off Scenes
    Noscar Nominations 2020
    @meeroslav
    NOSCAR Office Nominees History Book Magic Carpet

    View Slide

  13. NOSCAR NOMINEES
    NOSCAR
    LIVE TUESDAY DECEMBER 1 16:15 on WDLW
    @meeroslav
    Office Nominees History Book Magic Carpet
    Load Nominees
    loading = true

    View Slide

  14. NOSCAR NOMINEES
    NOSCAR
    LIVE TUESDAY DECEMBER 1 16:15 on WDLW
    LOADING…PLEASE WAIT
    @meeroslav
    Office Nominees History Book Magic Carpet
    loading = false
    nominees = payload
    Nominees Loaded
    Load Nominees
    loading = true

    View Slide

  15. NOSCAR NOMINEES
    NOSCAR
    LIVE TUESDAY DECEMBER 1 16:15 on WDLW
    MOST REVEALING TRAILER
    · THE VAMPIRE STRIKES BACK
    · SILENCE OF THE LAMPS
    · LORD OF THE DRINKS
    · PRESIDENT EVIL
    · GAME OF DRONES
    · LIE HARD
    MOST RIP-OFF SCENES MOST PREDICTABLE PLOT
    @meeroslav
    Office Nominees History Book Magic Carpet
    Load Nominees
    loading = true
    loading = false
    nominees = payload
    Nominees Loaded

    View Slide

  16. NOSCAR NOMINEES
    NOSCAR
    LIVE TUESDAY DECEMBER 1 16:15 on WDLW
    EVEN LIAM NEESON CAN’T FIND THAT
    @meeroslav
    Office Nominees History Book Magic Carpet
    Load Nominees
    loading = true
    loading = false
    error = Error404
    Nominees Load Failed

    View Slide

  17. export class LoadNominees implements Action {
    readonly type = NomineeActionTypes.LoadNominees;
    constructor(public payload: number) {}
    }
    export class NomineesLoaded implements Action {
    readonly type = NomineeActionTypes.NomineesLoaded;
    constructor(public payload: Nominee[]) {}
    }
    export class NomineesLoadFailed implements Action {
    readonly type = NomineeActionTypes.NomineesLoadFailed;
    constructor(public payload: number, public error: Error) {}
    }
    Action classes
    @meeroslav

    View Slide

  18. Time for
    generics!
    @meeroslav

    View Slide

  19. Multiple types
    class Student {
    firstName: string ;
    lastName: string ;
    get name() {
    return `${this.firstName} ${this.lastName}`;
    }
    constructor(firstName: string, lastName: string) {
    this.firstName = firstName ;
    this.lastName = lastName ;
    }
    }
    class Car {
    brand: string ;
    model: string ;
    constructor(brand: string, model: string) {
    this.brand = brand;
    this.model = model;
    }
    }
    class Pet {
    name: string ;
    constructor(name: string) {
    this.name = name;
    }
    }
    @meeroslav

    View Slide

  20. type agnostic function
    @meeroslav
    const nameLength = item item.name.length;
    const student = new Student(‘Jane', ‘Doe');
    const dog = new Pet(‘Dobbie');
    const suv = new Car('Toyota', 'RAV4');
    console.log(nameLength(student), nameLength(dog), nameLength(suv));

    View Slide

  21. interface HasName {
    name: string;
    }
    const nameLength = (item: T) item.name.length;
    const student = new Student('Jane', 'Doe');
    const dog = new Pet('Dobbie');
    const suv = new Car('Toyota', 'RAV4');
    console.log(nameLength(student), nameLength(dog), nameLength(suv));
    Controlled type agnostic function
    @meeroslav
    const suv: Car
    Argument of type 'Car' is not assignable to parameter of type 'HasName'.
    Property 'name' is missing in type 'Car' but required in type 'HasName'.ts(2345)

    View Slide

  22. export abstract class BaseAction implements Action {
    type = null; necessary evil
    constructor(public payload: T) {}
    }
    Generic Action classes
    @meeroslav

    View Slide

  23. Generic Action classes
    before
    export class LoadNominees implements Action {
    readonly type = `[Nominees] Load`;
    constructor(public payload: number) {}
    }
    with generic class
    export class LoadNominees implements BaseAction {
    readonly type = `[Nominees] Load`;
    }
    with typeless generic class
    export const LoadNominees implements BaseAction {} }
    with createAction function
    export const loadNominees = createAction(`[Nominees] Load`, props());
    @meeroslav

    View Slide

  24. Reducer
    @meeroslav

    View Slide

  25. export function nomineeReducer(state: NomineeState = initialState, action: NomineeActions): NomineeState {
    switch (action.type) {
    case NomineeActionTypes.LoadNominees: {
    return { …state, loading: LoadingState.LOADING, error: null };
    }
    case NomineeActionTypes. NomineesLoaded: {
    return { …state, loading: LoadingState.SUCCESSFUL,nominees: action.payload };
    }
    case NomineeActionTypes. NomineesLoadFailed: {
    return { …state, loading: LoadingState. FAILED,error: action.error };
    }
    default: {
    return state;
    }
    }
    }
    Reducer
    @meeroslav

    View Slide

  26. const featureReducer = createReducer(
    initialState,
    on(loadNominees, (state) ({ …state, loading: LoadingState.LOADING, error: null })),
    on(nomineesLoaded, (state, { nominees }) ({ …state, loading: LoadingState.SUCCESSFUL, nominees })),
    on(nomineesLoadFailed, (state, { error }) ({ …state, loading: LoadingState.FAILED, error }))
    );
    export function nomineeReducer(state: NomineeState, action: Action) {
    return featureReducer(state, action);
    }
    Reducer with functions
    @meeroslav

    View Slide

  27. Grouped Actions
    creators
    export const loadNominees = createAction(`${prefix} Load`,
    props());
    export const nomineesLoaded = createAction(`${prefix} Success`,
    props());
    export const loadNomineesFailed = createAction(`${prefix} Load Failed`,
    props());
    actions
    const myAction = loadNominees({ year: 2020, actionGroup: ActionGroup.LOAD });
    dedicated creator factories
    export const loadNominees = createLoadAction(`${prefix} Load`, props());
    export const nomineesLoaded = createSuccessAction(`${prefix} Success`,
    props());
    export const loadNomineesFailed = createFailureAction(`${prefix} Load Failed`,
    props());
    new usage
    const myAction = loadNominees({ year: 2020 });
    @meeroslav

    View Slide

  28. Grouped Reducer
    standard way
    const featureReducer = createReducer (
    initialState,
    on(loadNominees, (state) ({ …state, loading: LoadingState. LOADING, error: null })),
    on(nomineesLoaded, (state, { nominees }) ({ …state, loading: LoadingState.SUCCESSFUL, nominees })),
    on(nomineesLoadFailed, (state, { error }) ({ …state, loading: LoadingState.FAILED, error }))
    );
    export function nomineeReducer(state: NomineeState, action: Action): NomineeState {
    return featureReducer(state, action);
    }
    grouped reducer
    const successReducer = createReducer (
    initialState,
    on(nomineesLoaded, (state, { nominees }) ({ …state, loading: LoadingState.SUCCESSFUL, nominees })));
    export function nomineeReducer(state: NomineeState, action: Action): NomineeState {
    return createGroupedReducer(initialState, actions, { successReducer })(state, action);
    }
    @meeroslav

    View Slide

  29. export function createGroupedReducer(
    initialState: S,
    creators: { [key: string]: ActionCreator } ,
    config GroupedReducers
    ): ActionReducer {
    const creatorTypes: Record = Object.keys(creators)
    .reduce((acc, cur) ({ …acc, [creators[cur].type]: cur }), {});
    return (state: S = initialState, action: A ): S {
    if (!creatorTypes[action.type]) {
    return state;
    }
    if (isGroupedAction(action)) {
    if (action.actionGroup ActionGroup.LOAD) {
    return config.loadReducer ? config.loadReducer(state, action) : defaultLoadReducer(state);
    }
    if (action.actionGroup ActionGroup.SUCCESS) {
    return config.successReducer ? config.successReducer(state, action) : defaultSuccessReducer(state);
    }
    if (action.actionGroup ActionGroup.FAILURE) {
    return config.failureReducer ? config.failureReducer(state, action) : defaultFailedReducer(state, action);
    }
    throw new Error(`Unexpected action group ${action.actionGroup}.`);
    }
    return config.generalReducer ? config.generalReducer(state, action) : defaultGeneralReducer(state);
    };
    }
    Grouped Reducer under the hood
    @meeroslav

    View Slide

  30. Effects
    @meeroslav

    View Slide

  31. Effects
    @Injectable()
    export class NomineeEffects {
    loadNominees$ = createEffect(() this.actions$
    .pipe(
    ofType(loadNominees),
    switchMap(({ type, …payload }) this.service.getNominees({ type, …payload })
    .pipe(
    map(nominees nomineesLoaded({ nominees })),
    catchError(error of(nomineesLoadFailed({ error, …payload })))
    )
    )
    ));
    constructor(private readonly actions$: Actions, private readonly service: Nominee Service ){}
    }
    @meeroslav

    View Slide

  32. Effects
    @Injectable()
    export class NomineeEffects {
    loadNominees$ = this.createSwitchMapEffect(
    loadNominees,
    this.service.getNominees,
    () map(nominees nomineesLoaded({ nominees })),
    nomineesLoadFailed
    );
    constructor(private readonly actions$: Actions, private readonly service: Nominee Service ){}
    }
    @meeroslav

    View Slide

  33. protected createConcatMapEffect, RES = unknown>(
    allowedTypes: TypeOrCreator | [TypeOrCreator, …Array],
    method: (action: Omit) Observable,
    innerPipe: (action: Action & REQ) OperatorFunction,
    failureTarget: ActionCreator
    ): Observable {
    return createEffect(() this.actions$.pipe(
    ofType(…Array.isArray(allowedTypes) ? allowedTypes : [allowedTypes]),
    concatMap(({ type, …request }: Action & REQ) method(request)
    .pipe(
    this.mapInnerPipe(innerPipe, failureTarget, { type, …request } as Action & REQ))
    )
    ));
    }
    private mapInnerPipe(
    innerPipe: (action: REQ) OperatorFunction,
    failureTarget: ActionCreator,
    action: Action & REQ
    ): OperatorFunction {
    const { type, actionGroup, …payload } = action as GroupedAction;
    return pipe.call(this,
    innerPipe(action),
    catchError(error of(failureTarget({ …payload, error })))
    );
    }
    Inside the Machine
    @meeroslav

    View Slide

  34. Generics
    all
    the
    things!
    @meeroslav

    View Slide

  35. export abstract class Pageable {
    items: Array = [];
    skip = 0 ;
    private itemsCount: number;
    get hasMoreItems(): boolean { return this.itemsCount > PAGE_LIMIT; }
    get page(): number { return this.skip / PAGE_LIMIT + 1; }
    constructor (items: T[], skip: number = 0) {
    this.itemsCount = items.length;
    this.items = items.slice(0, PAGE_LIMIT);
    this.skip = skip;
    }
    }
    usage
    export class Nominees extends Pageable { }
    Pageable collection
    @meeroslav
    This is our wrapper for paginable collections. The actual implementation has more juice but in a nutshell we use generics to add common functionality to different types.

    View Slide

  36. Different
    Set
    of
    glasses
    @meeroslav

    View Slide

  37. Goodbye playlist
    Krewella - Say Goodbye
    Andrea Bocelli - Time to say goodbye (Con te partiro)
    Apparat - Goodbye
    Placebo - Song to say goodbye
    Green Day - Say goodbye
    Cage the Elephant - Goodbye
    Madsen - Goodbye Logik
    The Hoosiers - Goodbye Mr A
    Auf Wiedersehen - Zdravo Dovidjenja
    My Fearless Friend - Goodbye
    bit.ly/goodbye - boilerplate
    @meeroslav

    View Slide

  38. @meeroslav
    bit.ly/goodbye - boilerplate
    Stay Safe
    Stay Healthy
    Use Generics

    View Slide