TypeScript with Middleware
v2.0.0+

Some lifecycle hooks can be used to mutate both values and types at key points in a particular lifecycle. The SDK leverages TypeScript's inference powers to make writing middleware easy for both the developer and the user.

Mutating input

We can alter the input arguments for a function run (event data, step tooling, etc.), using onFunctionRun's transformInput() hook. (See Middleware - Lifecycle - Hook reference for a list of all available hooks.)

inngest/middleware/inputAlteration.ts

new InngestMiddleware({
  name: "Input Alteration",
  init() {
    return {
      onFunctionRun() {
        return {
          transformInput() {
            return {
              ctx: {
                /**
                 * This is a new property that will appear in my function.
                 */
                foo: "bar",
              },
            };
          },
        };
      },
    };
  },
});

inngest/fns/myFunction.ts

inngest.createFunction(
  { id: "example-function" },
  { event: "app/user.created" },
  async ({ event, foo }) => {
    //             ^? (parameter) foo: string
  }
);

As you can see above, all of our functions now have access to the foo variable within our function's input, including typing from the SDK inferring the alterations.

We can use this pattern to add additional typed tooling to function runs.

When returning a new ctx object, only specify the properties you wish to mutate. Omitting a property will use the default provided by the library, keeping complex types intact.

Advanced mutation

When middleware runs and transformInput() returns a new ctx, the types and data within that returned ctx are merged on top of the default provided by the library. This means that you can use a few tricks to overwrite data and types safely and more accurately.

For example, here we use a const assertion to infer the literal value of our foo example above.

// In middleware
transformInput() {
  return {
    ctx: {
      foo: "bar",
    } as const,
  };
}

// In a function
async ({ event, foo }) => {
  //             ^? (parameter) foo: "bar"
}

Because the returned ctx object and the default are merged together, sometimes good inferred types are overwritten by more generic types from middleware. A common example of this might be when handling event data in middleware.

To get around this, you can provide the data but omit the type by using an as type assertion. For example, here we use a type assertion to add foo and alter the event data without affecting the type.

async transformInput({ ctx }) {
  const event = await decrypt(ctx.event);

  const newCtx = {
    foo: "bar",
    event,
  };

  return {
    // Don't affect the `event` type
    ctx: newCtx as Omit<typeof newCtx, "event">,
  };
},

Ordering middleware and types

Middleware runs in the order specified when registering it (see Middleware - Lifecycle - Registering and order), which affects typing too.

When inferring a mutated input or output, the SDK will apply changes from each middleware in sequence, just as it will at runtime. This means that for two middlewares that add a foo value to input arguments, the last one to run will be what it seen both in types and at runtime.