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

    many places with little or no alteration … @meeroslav
  2. 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
  3. NOSCAR NOMINEES NOSCAR LIVE TUESDAY DECEMBER 1 16:15 on WDLW

    @meeroslav Office Nominees History Book Magic Carpet Load Nominees loading = true
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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));
  10. 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)
  11. export abstract class BaseAction<T> implements Action { type = null;

    necessary evil constructor(public payload: T) {} } Generic Action classes @meeroslav
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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.
  22. 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