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

Hello generics, goodbye boilerplate

Hello generics, goodbye boilerplate

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š

January 24, 2020
Tweet

More Decks by Miroslav Jonaš

Other Decks in Programming

Transcript

  1. Hello generics, goodbye boilerplate @meeroslav

  2. Hello generics, goodbye boilerplate @meeroslav

  3. code All developers love to @meeroslav

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

  5. code All developers love to copy/paste boilerplate @meeroslav

  6. Johnson vs Boilerplate @meeroslav

  7. …sections of code that have to be included in many

    places with little or no alteration… @meeroslav
  8. 90% @meeroslav

  9. Miroslav Jonaš @meeroslav

  10. First there was enterprise... @meeroslav

  11. Then came the state machine @meeroslav

  12. NOSCAR Office Nominees History Book Magic Carpet MOST REVEALING TRAILER

    MOST UNSUITABLE SCORE MOST PREDICTABLE PLOT MOST RIP-OFF SCENES Most Revealing Trailer Noscar Nominations 2020 Most Rip-off Scenes Noscar Nominations 2020 Most Unsuitable Score Noscar Nominations 2020 Most Predictable Plot Noscar Nominations 2020 LIVE FRIDAY JANUARY 24 16:45 on AGNTCNF @meeroslav
  13. LIVE FRIDAY JANUARY 24 16:45 on AGNTCNF NOSCAR NOMINEES NOSCAR

    Office Nominees History Book Magic Carpet Load Nominees @meeroslav
  14. LIVE FRIDAY JANUARY 24 16:45 on AGNTCNF NOSCAR NOMINEES LOADING…PLEASE

    WAIT NOSCAR Office Nominees History Book Magic Carpet Load Nominees loading = true @meeroslav
  15. LIVE FRIDAY JANUARY 24 16:45 on AGNTCNF NOSCAR NOMINEES LOADING…PLEASE

    WAIT NOSCAR Office Nominees History Book Magic Carpet Load Nominees loading = true loading = false nominees = payload Nominees Loaded <Nominees> @meeroslav
  16. LIVE FRIDAY JANUARY 24 16:45 on AGNTCNF NOSCAR NOMINEES MOST

    REVEALING TRAILER MOST RIP-OFF SCENES •THE VAMPIRE STRIKES BACK •SILENCE OF THE LAMPS •LORD OF THE DRINKS MOST PREDICTABLE PLOT •PRESIDENT EVIL •GAME OF DRONES •LIE HARD NOSCAR Office Nominees History Book Magic Carpet Load Nominees loading = false nominees = payload loading = true Nominees Loaded <Nominees> @meeroslav
  17. LIVE FRIDAY JANUARY 24 16:45 on AGNTCNF NOSCAR NOMINEES NOSCAR

    Office Nominees History Book Magic Carpet EVEN LIAM NEESON CAN’T FIND THAT URL Load Nominees loading = false error = Error404 loading = true Nominees Load Failed { status: 404 } @meeroslav
  18. Action classes 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) {} } @meeroslav
  19. Action classes 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) {} } @meeroslav
  20. Time for generics! @meeroslav

  21. 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; } } @meeroslav class Pet { name: string; constructor(name: string) { this.name = name; } }
  22. type agnostic function 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)); @meeroslav
  23. Controlled type agnostic function interface HasName { name: string; }

    const nameLength = <T extends HasName>(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)); "// TS234: Argument of type 'Car' is not assignable to parameter of type ‘HasName'. Property 'name' is missing in type 'Car' but required in type ‘HasName'. @meeroslav
  24. Generic Action classes export abstract class BaseAction<T> implements Action {

    type = null; "// necessary evil constructor(public payload: T) { } } @meeroslav
  25. "// before export class LoadNominees implements Action { readonly type

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

  27. Reducer 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; } } } @meeroslav
  28. Reducer with functions 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); } @meeroslav
  29. Grouped Actions "// creators export const loadNominees = createAction(`${prefix} Load`,

    props<{ year: number, actionGroup: ActionGroup.LOAD }>()); export const nomineesLoaded = createAction(`${prefix} Success`, props<{ nominees: Nominee[], actionGroup: ActionGroup.SUCCESS }>()); export const loadNomineesFailed = createAction(`${prefix} Load Failed`, props<{ year: number, error: Error, actionGroup: ActionGroup.FAILURE }>()); "// actions const myAction = loadNominees({ year: 2020, actionGroup: ActionGroup.LOAD }); "// dedicated creator factories export const loadNominees = createLoadAction(`${prefix} Load`, props<{ year: number }>()); export const nomineesLoaded = createSuccessAction(`${prefix} Success`, props<{ nominees: Nominee[] }>()); export const loadNomineesFailed = createFailureAction(`${prefix} Load Failed`, props<{ year: number, error: Error }>()); "// new usage const myAction = loadNominees({ year: 2020 }); @meeroslav
  30. 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
  31. Grouped Reducer under the hood export function createGroupedReducer<S extends BaseState,

    A extends Action>( initialState: S, creators: { [key: string]: ActionCreator<string, Creator> }, config"?: GroupedReducers<S, A> ): ActionReducer<S, A> { const creatorTypes: { [key: string]: string } = 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, acti } throw new Error(`Unexpected action group ${action.actionGroup}.`); } return config.generalReducer ? config.generalReducer(state, action) : defaultGeneralReducer(state); }; } @meeroslav
  32. Effects @meeroslav

  33. Effects @Injectable() export class NomineeEffects { loadNominees$ = createEffect(() "=>

    this.actions$ .pipe( ofType(loadNominees), switchMap(({ type, …payload }) "=> this.service.getNominees({ type, …payload }) .pipe( map(response "=> nomineesLoaded({ nominees: response })), catchError(error "=> of(nomineesLoadFailed({ error, …payload }))) ) ) )); constructor(private readonly actions$: Actions, private readonly service: NomineeService){} } @meeroslav
  34. 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: NomineeService){} } @meeroslav
  35. protected createConcatMapEffect<REQ extends object = {}, RES = any>( allowedTypes:

    TypeOrCreator | [TypeOrCreator, ""...Array<TypeOrCreator>], method: (action: REQ) "=> Observable<RES>, innerPipe: (action: REQ) "=> OperatorFunction<RES, Action>, failureTarget: ActionCreator<string, Creator> ): Observable<Action> { return createEffect(() "=> this.actions$ .pipe( ofType(""...Array.isArray(allowedTypes) ? allowedTypes : [allowedTypes]), concatMap((action: REQ & Action) "=> method(action) .pipe(this.mapInnerPipe<REQ & Action, RES>(innerPipe, failureTarget, action)) ) )); } private mapInnerPipe<REQ extends Action, RES = any>( innerPipe: (action: REQ) "=> OperatorFunction<RES, Action>, failureTarget: ActionCreator<string, Creator>, action: REQ ): OperatorFunction<RES, Action> { const { type, actionGroup, ""...payload } = action as any; return pipe.call(this, innerPipe(action), catchError(error "=> of(failureTarget({ ""...payload, error }))) ); } Inside the Machine @meeroslav
  36. Generics all the things! @meeroslav

  37. Pageable collection export abstract class Pageable<T> { items: Array<T> =

    []; 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<Nominee> {} @meeroslav
  38. Different Set of glasses @meeroslav

  39. 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
  40. @meeroslav bit.ly/ng-helper bit.ly/goodbye-boilerplate

  41. Fun skiing/boarding!