Issues with generic types and defaults when using TypeScript in strict mode

I’m currently refactoring a codebase to support TypeScript’s strict mode. However, I’ve ran into an issue with functions that leverage generics.

In essence, I have a function that takes an object with a blocks property, this property uses a type that is generic, but has default types.

This is the definition of the object:

export type Context<
  S extends VariableSchema = VariableSchema,
  R extends VariableSchema = VariableSchema,
> = {
  blocks: { [key: string]: Block };
  schema: S;
  result: R;
};

The Block type itself is generic as well and looks like this:

export type Block<
  ParamMap extends BlockParamSchema = BlockParamSchema,
  HandleMap extends { [key: string]: string } = { [key: string]: string },
> = {
  label: string;
  parameters: ParamMap;
  validateParameters: (args: {
    parameters: ConvertVariableSchemaToObjectType<ParamMap>;
    getVariableDefinition: (name: string) => Variable;
  }) => z.AnyZodObject;
  handles: HandleMap;
  execute: (args: BlockExecutionApi<ParamMap, HandleMap>) => Promisable<void>;
};

I then use it in the following manner:

export const deductionCalculationContext = createContext({
  blocks: defaultBlocks,
  schema: {
    deductionYtd: v.number('Deduction YTD'),
    eligibleWages: v.number('Eligible Wages'),
  },
  result: {
    employeeAmount: v.number('Employee Amount'),
    companyAmount: v.number('Company Amount'),
  },
});

All Blocks are declared using a createBlock function which infers the generic types of Block for type safety. Specifically, it ensures that when a developer passes a set of parameters, the validator arguments are typed based on that:

export const conditionBlock = createBlock({
  label: 'If',

  parameters: {
    when: {
      label: 'Condition',
      type: 'unknown',
      required: true,
    },
    equalTo: {
      label: 'Equal to',
      type: 'unknown',
      required: true,
    },
  },

  handles: {
    true: 'True',
    false: 'False',
  },

  execute: async ({ parameters, fireHandle }) => {
    parameters.when === parameters.equalTo ? fireHandle('true') : fireHandle('false');
  },
});

However even though it’s the same type, less the generic args, TS won’t compile, displaying this error:

TypeScript error

I’ve tried the following things:

  • Omitting the blocks and redeclaring it without generics works, but I’m afraid of the type safety issues it’ll bring.

  • Disabling strictFunctionTypes also works, but breaks other type safety features related to functions.

  • Making the blocks property generic.

  • Making the blocks property generic, but without a generic type constraint, and then using a mapped type, conditional types, and inference to get the values of the generics. This worked in concept but I can’t apply it since it’d reference the type recursively.

I’m trying to avoid having to explicitly pass the generic type, and I know there are libraries out there that have accomplished this.