Search code examples
javascripttypescriptrefactoringhigher-order-functions

JS TS how to refactor repetitive code into a higher order function with multiple param types


I've been wanting to refactor the following code into a higher order function with typescript to make it cleaner and more re-usable, though have been finding it really challenging to make it work.

import { DocumentDefinition, FilterQuery, QueryOptions, UpdateQuery } from 'mongoose';
import TaskModel, { TaskDocument } from '../models/Task.model';
import { databaseResponseTimeHistogram } from '../utils/appmetrics';

export async function createTask(
    input: DocumentDefinition<
        Omit<TaskDocument, 'createdAt' | 'updatedAt' | 'taskId' | 'isCompleted'>
    >
) {
    const metricsLabels = { operation: 'createTask' };
    const timer = databaseResponseTimeHistogram.startTimer();

    try {
        const result = TaskModel.create(input);
        timer({ ...metricsLabels, success: 'true' });

        return result;
    } catch (err: any) {
        timer({ ...metricsLabels, success: 'false' });
        throw new Error(err.message);
    }
}

export async function findTask(
    query: FilterQuery<TaskDocument>,
    options: QueryOptions = { lean: true }
) {
    const metricsLabels = { operation: 'findTask' };
    const timer = databaseResponseTimeHistogram.startTimer();

    try {
        const result = TaskModel.findOne(query, {}, options);
        timer({ ...metricsLabels, success: 'true' });

        return result;
    } catch (err: any) {
        timer({ ...metricsLabels, success: 'false' });
        throw new Error(err.message);
    }
}

export async function findAndUpdateTask(
    query: FilterQuery<TaskDocument>,
    update: UpdateQuery<TaskDocument>,
    options: QueryOptions
) {
    const metricsLabels = { operation: 'findTask' };
    const timer = databaseResponseTimeHistogram.startTimer();

    try {
        const result = TaskModel.findOneAndUpdate(query, update, options);
        timer({ ...metricsLabels, success: 'true' });

        return result;
    } catch (err: any) {
        timer({ ...metricsLabels, success: 'false' });
        throw new Error(err.message);
    }
}

Basically I am wanting to refactor the whole metrics functionality with the try catch block into a utility function, allowing to call it with the respective parameters, the operation, the TaskModel.method and corresponding params with would be (input) for create, (query, {}, options) for findOne and (query, update, options) for findManyAndUpdate...

So far have I ran into difficulties with correct typing of all the different params etc.


Solution

  • So basically you want to refactor this part:

    try {
        /* SOME OPERATION */
        timer({ ...metricsLabels, success: 'true' });
    
        return result;
    } catch (err: any) {
        timer({ ...metricsLabels, success: 'false' });
        throw new Error(err.message);
    }
    

    You can just wrap it in a function and pass in the SOME OPERATION as an argument:

    function withTimer (label:string, operation:Function) {
        const metricsLabels = { operation: label };
        const timer = databaseResponseTimeHistogram.startTimer();
    
        try {
            const result = operation();
            timer({ ...metricsLabels, success: 'true' });
    
            return result;
        } catch (err: any) {
            timer({ ...metricsLabels, success: 'false' });
            throw new Error(err.message);
        }
    }
    

    Now your functions can be rewritten as:

    export async function createTask(
        input: DocumentDefinition<
            Omit<TaskDocument, 'createdAt' | 'updatedAt' | 'taskId' | 'isCompleted'>
        >
    ) {
        return withTimer('createTask',
            () => TaskModel.create(input)
        );
    }
    
    export async function findTask(
        query: FilterQuery<TaskDocument>,
        options: QueryOptions = { lean: true }
    ) {
        return withTimer('findTask',
            () => TaskModel.findOne(query, {}, options)
        );
    }
    
    export async function findAndUpdateTask(
        query: FilterQuery<TaskDocument>,
        update: UpdateQuery<TaskDocument>,
        options: QueryOptions
    ) {
        return withTimer('findTask',
            () => TaskModel.findOneAndUpdate(query, update, options)
        );
    }
    

    The key to using higher-order functions is realizing that you can wrap the uncommon parts of code in functions to be passed to the common parts of code.