UtilitiesSelecting Handlers by Type

Utilities

Selecting Handlers by Type

An in-depth guide to the zet utility for selecting handlers based on type.


Selecting Handlers by Type

Workflow Kit uses extensively the zet utility function.

This utility function creates a function that maps an object to the correct handler based on a specific property inside that object.

To illustrate better how it works imagine we had the following types:

interface Person<T extends Vehicle> {
  name: string;
  age: number;
  vehicle: T;
}

type Vehicle = Car | Bike;

interface Car {
  details: {
    type: "car";
    fuel: "electric" | "gas";
    model: string;
    year: number;
  };
  performance: {
    horsepower: number;
    topSpeed: number; // in km/h
  };
}

interface Bike {
  details: {
    type: "bike";
    gear: "fixed" | "multi";
    brand: string;
  };
  specifications: {
    weight: number; // in kg
    frameMaterial: string;
  };
}

If we wanted to create a function called generateMessage that receives a person as argument and returns different data depending on the vehicle that the person has we could use the zet function the following way:

import { zet } from "@/shared/lib/zet";

interface Person<T extends Vehicle> {
  name: string;
  age: number;
  vehicle: T;
}

type Vehicle = Car | Bike;

interface Car {
  details: {
    type: "car";
    fuel: "electric" | "gas";
    model: string;
    year: number;
  };
  performance: {
    horsepower: number;
    topSpeed: number; // in km/h
  };
}

interface Bike {
  details: {
    type: "bike";
    gear: "fixed" | "multi";
    brand: string;
  };
  specifications: {
    weight: number; // in kg
    frameMaterial: string;
  };
}

interface Zet {
  object: Person<Vehicle>;
  nested: ["vehicle"];
  filter: ["details", "type"];
  params: [];
  return: string;
}

const dispatch = zet<Zet>(["vehicle"], ["details", "type"], {
  car: (person: Person<Car>) => {
    return `This person drives a ${person.vehicle.details.fuel} car.`;
  },
  bike: (person: Person<Bike>) => {
    return `This person rides a ${person.vehicle.details.gear} bike.`;
  },
});

function generateMessage(person: Person<Vehicle>): string {
  return dispatch(person);
}

const personWithCar: Person<Car> = {
  name: "John",
  age: 25,
  vehicle: {
    details: {
      type: "car",
      fuel: "gas",
      model: "Toyota Corolla",
      year: 2020,
    },
    performance: {
      horsepower: 132,
      topSpeed: 180,
    },
  },
};

console.log(generateMessage(personWithCar)); // This person drives a gas car.

The type that the function receives is an object with the following properties:

  • object: The input object.
  • nested: The path to the property that contains the union you want to narrow.
  • filter: The path to the key in the union whose value determines which handler to use.
  • params: Extra arguments to pass to the returned function.
  • return: The return type of the returned function.

The function parameters that the function receives are the following:

  • The path to the property that contains the union you want to narrow.
  • The path to the key in the union whose value determines which handler to use.
  • An object where each property points to the handler function that should run.

Refine Utility Type

If Person was not generic, we would need to use the Refine utility type:

import { zet, type Refine } from "@/shared/lib/zet";

interface Person {
  name: string;
  age: number;
  vehicle: Vehicle;
}

type Vehicle = Car | Bike;

interface Car {
  details: {
    type: "car";
    fuel: "electric" | "gas";
    model: string;
    year: number;
  };
  performance: {
    horsepower: number;
    topSpeed: number; // in km/h
  };
}

interface Bike {
  details: {
    type: "bike";
    gear: "fixed" | "multi";
    brand: string;
  };
  specifications: {
    weight: number; // in kg
    frameMaterial: string;
  };
}

type PersonWithCar = Refine<{
  object: Person;
  nested: ["vehicle"];
  filter: ["details", "type"];
  narrow: "car";
}>;

type PersonWithBike = Refine<{
  object: Person;
  nested: ["vehicle"];
  filter: ["details", "type"];
  narrow: "bike";
}>;

interface Zet {
  object: Person;
  nested: ["vehicle"];
  filter: ["details", "type"];
  params: [];
  return: string;
}

const dispatch = zet<Zet>(["vehicle"], ["details", "type"], {
  car(person: PersonWithCar) {
    return `This person drives a ${person.vehicle.details.fuel} car.`;
  },
  bike(person: PersonWithBike) {
    return `This person rides a ${person.vehicle.details.gear} bike.`;
  },
});

function generateMessage(person: Person): string {
  return dispatch(person);
}

const personWithCar: PersonWithCar = {
  name: "John",
  age: 25,
  vehicle: {
    details: {
      type: "car",
      fuel: "gas",
      model: "Toyota Corolla",
      year: 2020,
    },
    performance: {
      horsepower: 132,
      topSpeed: 180,
    },
  },
};

console.log(generateMessage(personWithCar)); // This person drives a gas car.