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

  2. developers love to code @meeroslav

  3. developers love to copy/paste code @meeroslav

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

  5. Johnson vs Boilerplate @meeroslav

  6. … sections of code that have to be included in

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

  8. Miroslav Jonaš @meeroslav

  9. First there was enterprise... @meeroslav

  10. Then came the state machine @meeroslav

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

  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
  13. NOSCAR NOMINEES NOSCAR LIVE TUESDAY DECEMBER 1 16:15 on WDLW

    @meeroslav Office Nominees History Book Magic Carpet Load Nominees loading = true
  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
  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
  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
  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
  18. Time for generics! @meeroslav

  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
  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));
  21. 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)); 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)
  22. export abstract class BaseAction<T> implements Action { type = null;

    necessary evil constructor(public payload: T) {} } Generic Action classes @meeroslav
  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<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 }>()); @meeroslav
  24. Reducer @meeroslav

  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
  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
  27. 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
  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
  29. 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: Record<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, action); } throw new Error(`Unexpected action group ${action.actionGroup}.`); } return config.generalReducer ? config.generalReducer(state, action) : defaultGeneralReducer(state); }; } Grouped Reducer under the hood @meeroslav
  30. Effects @meeroslav

  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
  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
  33. protected createConcatMapEffect<REQ extends Record<string, unknown>, RES = unknown>( allowedTypes: TypeOrCreator

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

  35. 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> { } 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.
  36. Different Set of glasses @meeroslav

  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
  38. @meeroslav bit.ly/goodbye - boilerplate Stay Safe Stay Healthy Use Generics