I'm trying to create a React higher-order component for a specific use-case, the problem is boiling down to the following:
function sample<TObj, P extends keyof TObj, F extends keyof TObj>(
obj: TObj,
prop: P,
setProp: TObj[F] extends (value: TObj[P]) => void ? F : never
) {
obj[setProp](obj[prop]);
}
I want to be able to pass an object, a string which should be a key of that object, and another key of that object but which is required to be a function.
This can be simplified further as such:
function sample2<TObj, F extends keyof TObj>(
obj: TObj,
setProp: TObj[F] extends () => void ? F : never
) {
obj[setProp]();
}
It seems to me that because I use the conditional type, it can be guaranteed that obj[setProp]
will be a function but I get the error:
This expression is not callable.
Type 'unknown' has no call signatures.ts(2349)
As can be seen below, the function will error if it will be called with a key that doesn't respect the requirement. But that same requirement doesn't seem to be applied inside the function.
I understand that this could be seen as a XY problem, but it got me really interested in whether there is a way to make this specific problem work correctly.
Inside the implementation of sample2()
, the type TObj[F] extends () => void ? F : never
is an unresolved conditional type. That is, it's a conditional type that depends on a currently-unspecified generic type parameter to be resolved. In such cases, the compiler generally doesn't know what to do with it and treats it as essentially opaque. (See microsoft/TypeScript#23132 for some discussion of this.) In particular it doesn't realize that TObj[Tobj[F] extends ()=>void ? F : never]
will ultimately have to resolve to some subtype of ()=>void
.
In general I'd avoid conditional types entirely unless they are necessary. The compiler can more easily understand and infer from mapped types like Record<K, V>
:
function sample2<K extends PropertyKey, T extends Record<K, () => void>>(
obj: T,
prop: K
) {
obj[prop]();
}
And that behaves similarly when you call it:
const obj2 = {
func() { console.log("func") },
prop: 42
};
sample2(obj2, "func"); // okay,
//sample2(obj, "prop"); // error
// ~~~ <-- number is not assignable to ()=>void
EDIT: to address the original sample()
, I'd use this definition:
function sample<
PK extends PropertyKey,
FK extends PropertyKey,
T extends Record<PK, any> & Record<FK, (v: T[PK]) => void>
>(
comp: T,
prop: PK,
setProp: FK
) {
comp[setProp](comp[prop]);
}
const obj = {
func(z: number) { console.log("called with " + z) },
prop: 42
}
which, I think, also behaves how you'd like:
sample(obj, "prop", "func"); // called with 42
sample(obj, "prop", "prop"); // error!
// ~~~ <-- number not assignable to (v: number)=>void
sample(obj, "func", "func"); // error!
// ~~~ <-- (v: number)=>void not assignable to number
Okay, hope that helps; good luck!