The conventional way to do this is to make the func
method generic in the type of the name
parameter which should be constrained to keyof T
, like this:
type ObjectWithFunc<T extends { [name: string]: any[] }> = {
func: <Name extends keyof T>(name: Name, ...args: T[Name]) => void
}
declare let objectWithFunc: ObjectWithFunc<T1>;
objectWithFunc.func('n1', 10); // okay
objectWithFunc.func('n2', "abc", true); // okay
objectWithFunc.func('n2', 123); // error!
From a type system point of view, you're trying to say that the func
method of a value of type ObjectWithFunc<T>
should behave like (name: K, ...args: T[K])=>void
for every key K
in keyof T
. That's basically an intersection of function types, and thus corresponds to overloads. For example:
type OWFT1Func =
((name: 'n1', arg0: number) => void) &
((name: 'n2', arg0: string, arg1: boolean) => void);
let f: OWFT1Func = objectWithFunc.func; // this assignment is allowed
f('n1', 10); // okay
f('n2', "abc", true); // okay
f('n2', 123); // error!
As you can see, the type OWFT1Func
has multiple call signatures and is therefore an overloaded function, and it behaves similarly to the generic version above. You can think of generics in TypeScript as an arbitrary or potentially infinite intersection.
In generic functions and in intersections of functions, the caller can specify the call signature or generic type parameters, and the implementer needs to be able to handle any such choice. Freedom for the caller becomes a constraint for the implementer.
Compare this to what you were trying to do: your ValueOf<{ [Name in keyof T]: ((name:Name, ...args:T[Name])=>void) }>
is a union of function types instead of an intersection. Thus, instead of saying that it should behave like every call signature you care about, you were saying that it behaves like some call signature you care about. That's not what you meant.
And it's also not a very useful type for a caller; if all you know is that you have some call signature from a set, you quite often cannot call it at all, since you don't know which one it is. The only way to safely call a union of functions is if there's some call that would work for every possible call signature at once. Meanwhile the implementer can just happily pick one of the functions to implement and ignore the others. Freedom for the implementer becomes a constraint for the caller.
Notice the duality here: an intersection of functions lets the caller pick some call signature, while the implementer needs to write something that works for all of them at once; a union of functions lets the implementer pick some call signature, while the caller needs to write something that works for all of them at once. Another way to say this is that, in theory, an intersection of functions accepts a union of parameters, and a union of functions accepts an intersection of parameters.
This is a consequence of the contravariance of function types in the types of their parameters. (See Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for more information). Contravariance can be confusing, since it inverts the relationship between types. If F<T>
is contravariant in T
, then X extends Y
implies F<Y> extends F<X>
and not vice versa. Since string
is assignable to string | number
, then the function type (x: string | number) => void
is assignable to (x: string) => void
and not vice versa. So that's a possible source of your trouble when trying to solve this.
Playground link to code