You can help out type inference by tweaking makeInvalidatorRecord to be type-parameterized over the invalidators' argument types. This is easier to explain in code than prose.
Instead of this:
type invalidatorObj<T extends Record<string, unknown>> = {
[K in keyof T]: T[K] extends invalidator<infer U> ? invalidator<U> : never;
};
You could use a type like this:
type invalidatorObjFromArgs<T extends Record<string, unknown[]>> = {
[K in keyof T]: invalidator<T[K]>
};
And then have the type parameter of makeInvalidatorObj be a Record whose property values are argument tuples rather than entire invalidators.
Here's a complete example:
type invalidator<TArgs extends unknown[]> = {
fn: (...args: TArgs) => any;
genSetKey: (...args: TArgs) => string;
};
type invalidatorObjFromArgs<T extends Record<string, unknown[]>> = {
[K in keyof T]: invalidator<T[K]>
};
const makeInvalidatorObj = <T extends Record<string, unknown[]>>(
invalidatorObj: invalidatorObjFromArgs<T>,
) => invalidatorObj;
const inferredExample = makeInvalidatorObj({
updateName: {
fn: (name: string) => {},
genSetKey: (name) => `name:${name}`,
// ^? - (parameter) name: string
},
updateAge: {
fn: (age: number) => {},
genSetKey: (age) => `age:${age}`,
// ^? - (parameter) age: number
},
});