Reputation: 2686
I'm playing with the following snippet. When I remove the line validate: (...) => {}
, A
is correctly inferred as rule1: { v: number }
. However, when the line is not removed, A
is inferred as unknown
. Any ideas what causes this and how to fix it? Thanks in advance :)
type Arguments<A> = {
[K in keyof A]: {
args: {
[K1 in keyof A[K]]: {
assert?: (arg: unknown) => arg is A[K][K1];
}
};
validate?: (value: unknown, args: A[K]) => unknown;
}
}
function test<A>(args: Arguments<A>) {}
test({
rule1: {
args: {
v: {
assert: (arg): arg is number => typeof arg === 'number',
}
},
validate: (value, args) => {
}
}
});
Upvotes: 1
Views: 224
Reputation: 329953
The problem here is that the compiler needs to both infer the generic type parameter and the type of the unannotated callback parameter, inside the same object, and it can't really do that consistently. This is a known design limitation; see microsoft/TypeScript#12621 and microsoft/TypeScript#26418 among others. The short answer here is you either have to give up on one of these inferences (by either manually specifying the generic type parameter or by manually annotating the callback parameters), or you have to break the single object apart into multiple objects so that the compiler can do the multiple inference passes in stages.
The workaround I'll present here is a helper function which builds your single object from two separate ones. The first object will be used to infer the generic type parameter, which will then be used in the contextual type of the callback parameter in the second object. (I called this "currying" in the comments but it really isn't currying, sorry about the terminology error)
const arg = function <T>(
args: { [K in keyof T]: {
assert?: (arg: unknown) => arg is T[K];
} },
validate?: (value: unknown, args: T) => unknown
) {
return ({ args, validate });
}
The arg()
function takes two parameters, args
and validate
, and combines them into a single object. When you call test()
, now, you use arg()
to build each property:
test({
rule1: arg({
v: {
assert: (arg): arg is number => typeof arg === 'number',
}
}, (value, args) => { }
)
});
Here the type parameter in arg()
is inferred to be {v: number}
, and thus the type parameter in test()
is now properly inferred to be {rule1: {v: number}}
as desired. It's not perfect, but it's the best I can think of for now. Okay, hope that helps; good luck!
Upvotes: 1