Type functions based on property in object

Viewed 45

I'm trying to type an interface/object correctly so that I can make functions for each object with a certain type. I don't know how to try and explain it better so I will give you some code.

This is my interface:

export interface Activity {
  id: string;
  metaData:
    | {
        type: "createTask";
        data: { taskId: string; taskName: string; user: string };
        task: string;
        taskName: string;
      }
    | {
        type: "deleteTask";
        data: { taskName: string; user: string };
        task: string;
        taskName: string;
      }
    | {
        type: "updateTask";
        data: { taskId: string; taskName: string; user: string };
        task: string;
        taskName: string;
      }
    | {
        type: "createScenario";
        data: {
          scenarioId: string;
          scenarioName: string;
          taskName: string;
          user: string;
        };
        task: string;
        taskName: string;
      }
    | {
        type: "updateScenario";
        data: { scenarioId: string; scenarioName: string; user: string };
        task: string;
        scenario: string;
        taskName: string;
      }
    | {
        type: "deleteScenario";
        data: { scenarioName: string; user: string };
        task: string;
        taskName: string;
      };
}

And now from this interface I want to type a function for each different type of activity so that I can lookup that function based on the type.

This is what I tried:

export const activityFormatFunctions: Record<
  Activity["metaData"]["type"],
  (activity: Activity) => string
> = {
  createScenario: (activity) => {
    return ""
  },
  createTask: (activity) => {
    return ""
  },
  deleteScenario: (activity) => {
    return ""
  },
  deleteTask: (activity) => {
    return ""
  },
  updateScenario: (activity) => {
    activity.metaData.data.scenarioName;
    return ""
  },
  updateTask: (activity) => {
    activity.metaData.data.taskName;
    return ""
  },
};

The problem is that the "activity" parameter is not narrowed to the activity with the matching type making it so I can only access common properties between all of the different activity types.

In my example above I want to be able to access scenarioName in the updateScenario scenario function because the Activity interface says it has a scenarioName

{
  type: "updateScenario";
  data: { scenarioId: string; scenarioName: string; user: string };
  task: string;
  scenario: string;
  taskName: string;
}
1 Answers

Link to playground

I prefered to declare each Activity in separate interfaces for readability, and declare a union type, to have a fixed length of possibilities.

The big part of the solution lies on the type of your object containing your functions, and find the corresponding union type.

type FindByType<Union, Type> = Union extends { type: Type } ? Union : never;

type MetadataMap = {
    [K in Activity<TasksUnion>["metaData"]["type"]]: (activity: Activity<FindByType<TasksUnion, K>>) => string
}

export interface Activity<T extends TasksUnion> {
  id: string;
  metaData: T
}

type TasksUnion =
 | CreateTask
 | DeleteTask
 | UpdateTask
 | CreateScenario
 | UpdateScenario
 | DeleteScenario

type TaskType =
 | "createTask"
 | "deleteTask"
 | "updateTask"
 | "createScenario"
 | "updateScenario"
 | "deleteScenario"


interface BaseTask {
    type: TaskType;
}

interface CreateTask extends BaseTask {
    type: "createTask";
    data: { taskId: string; taskName: string; user: string };
    task: string;
    taskName: string;
}

interface DeleteTask extends BaseTask {
    type: "deleteTask";
    data: { taskName: string; user: string };
    task: string;
    taskName: string;
}

interface UpdateTask extends BaseTask {
    type: "updateTask";
    data: { taskId: string; taskName: string; user: string };
    task: string;
    taskName: string;
}

interface CreateScenario extends BaseTask {
    type: "createScenario";
    data: {
        scenarioId: string;
        scenarioName: string;
        taskName: string;
        user: string;
    };
    task: string;
    taskName: string;
}


interface UpdateScenario extends BaseTask {
    type: "updateScenario";
    data: { scenarioId: string; scenarioName: string; user: string };
    task: string;
    scenario: string;
    taskName: string;
}

interface DeleteScenario extends BaseTask {
    type: "deleteScenario";
    data: { scenarioName: string; user: string };
    task: string;
    taskName: string;
};

// Everything correctly typed 
export const activityFormatFunctions: MetadataMap = {
  createScenario: (activity) => {
    return ""
  },
  createTask: (activity) => {
    return ""
  },
  deleteScenario: (activity) => {
    return ""
  },
  deleteTask: (activity) => {
    return ""
  },
  updateScenario: (activity) => {
    activity.metaData.data.scenarioName;
    return ""
  },
  updateTask: (activity) => {
    activity.metaData.data.taskName;
    return ""
  }
}
Related