Here is my code (with renamed methods) from Angular:
I have a function interface/contract
type IAutocomlete = (
  term: string,
  selectedIds?: string[]
) => Observable<TableFilterQueryResponse[]>;
This is my draft service that I want to inject in my Components in the future. I will have more methods with similar implementation.
    export class SuperCleanService {
      constructor(
        private dataController: DataController,
        private serviceController: ServiceController
      ) {}
    .....
    private searchAs$: IAutocomlete = (term, selectedIds) => {
        return this.dataController
          .service1GET(term, selectedIds)
          .pipe(
            map((res) =>
              res.map((a) => ({
                viewValue: a.name,
                value: a.id
              }))
            )
          );
      };
      private searchB$: IAutocomlete = (term, selectedIds) => {
        return this.serviceController
          .service1(term, selectedIds)
          .pipe(
            map((res) =>
              res.map((a) => ({
                viewValue: a.id,
                value: a.id
              }))
            )
          );
      };
      private searchC$: IAutocomlete = (term, selectedIds) => {
        return this.dataController
          .service2GET(term, selectedIds)
          .pipe(
            map((res) =>
              res.map((a) => ({
                viewValue: a.name,
                value: a.id
              }))
            )
          );
      };
    ...
    }
How can I refactor (following DRY rule) and make it as clean as possible? My main goal is to avoid using map in each function.
Consider creating a custom operator that houses the reusable logic.
custom-pipe.ts
import { pipe, of } from "rxjs";
import { startWith, switchMap } from "rxjs/operators";
export function myCustomOperator() {
  return pipe(
    map((res) =>
      res.map((a) => ({
        viewValue: a.name,
        value: a.id
      }))
    )
  );
}
Your component will become:
export class SuperCleanService {
  constructor(
    private dataController: DataController,
    private serviceController: ServiceController
  ) {}
.....
  private searchAs$: IAutocomlete = (term, selectedIds) => {
    return this.dataController
      .service1GET(term, selectedIds)
      .pipe(
        myCustomOperator()
      );
  };
  private searchB$: IAutocomlete = (term, selectedIds) => {
    return this.serviceController
      .service1(term, selectedIds)
      .pipe(
        myCustomOperator()
      );
  };
  private searchC$: IAutocomlete = (term, selectedIds) => {
    return this.dataController
      .service2GET(term, selectedIds)
      .pipe(
        myCustomOperator()
      );
  };
...
}
                        In this case I would use generics with wrapper method which accepts autocomplete function and maps to desired result.
interface Autocomplete {
  id?: string;
  name?: string;
}
type IAutocomplete = (
  term: string,
  selectedIds?: string[]
) => Observable<TableFilterQueryResponse[]>;
type AutocompleteCallback<T> = (
  term: string,
  selectedIds?: string[]
) => Observable<T[]>;
...
  // use instance arrow function to preserve 'this' in callback method
  private autocompleteMapper = <T extends Autocomplete>(
    callback: AutocompleteCallback<T>,
    term: string,
    selectedIds: string[]
  ) => {
    return callback(term, selectedIds).pipe(
      map((res) =>
        res.map((a) => ({
          viewValue: a.name,
          value: a.id
        }))
      )
    );
  };
  private search1$: IAutocomplete = (term, selectedIds) => {
    return this.autocompleteMapper<ResponseInterface>(
      this.dataControllerService.service1GET,
      term,
      selectedIds
    );
  };
  ...
``
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With