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. …sections of code that have to be included in many

    places with little or no alteration… @meeroslav
  2. 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
  3. LIVE FRIDAY JANUARY 24 16:45 on AGNTCNF NOSCAR NOMINEES NOSCAR

    Office Nominees History Book Magic Carpet Load Nominees @meeroslav
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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; } }
  11. 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
  12. 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
  13. Generic Action classes export abstract class BaseAction<T> implements Action {

    type = null; "// necessary evil constructor(public payload: T) { } } @meeroslav
  14. "// 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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