Description
Consider the following function, based on this StackOverflow question:
// Take a type and some properties to pluck from it. Ensure only functions can be plucked.
type PluckFuncs =
<TComponent extends {[P in TKey]: Function }, TKey extends string>
(props: TKey[]) => any
Currently it is not possible to use this function ergonomically. You must always supply both type arguments, even though TKey
can comfortably be inferred from props
.
It should be possible to invoke a generic function and specify a named subset of the type parameters, i.e. pluckFuncs<TComponent = { a: () => string }>(['a'])
, and have inference decide (or fail to decide) the remaining type parameters (in this case TKey
).
In terms of the desired semantics, the inference should treat explicit type arguments no differently from the types of the function's value arguments, except for the fact that no values will be forthcoming at the invocation site.
In other words, doing this:
type PluckFuncs =
<TComponent extends {[P in TKey]: Function }, TKey extends string>
(props: TKey[]) => any
declare const pluckFuncs: PluckFuncs
pluckFuncs<{ a: () => string }>(['a']) // Won't work, forced to explicitly specify `TKey`
should behave no differently from when I just add a dummy value argument to serve as an inference site:
type PluckFuncs =
<TComponent extends {[P in TKey]: Function }, TKey extends string>
(props: TKey[], dummy?: TComponent) => any
declare const pluckFuncs: PluckFuncs
pluckFuncs(['a'], undefined as { a: () => string }) // Compiler is happy with this though
Aside:
The workaround one might come up with is to give TKey
a default of keyof TComponent
:
type PluckFuncs =
<TComponent extends {[P in TKey]: Function }, TKey extends string = keyof TComponent>
(props: TKey[]) => any
This seems like a sensible thing to do, but isn't really the semantics we're looking for. When you try to use it:
declare const pf: PluckFuncs
interface TestProps {a: number, b: Function, c: () => number}
// Good - missing property, should be an error
pf<TestProps>(['d'])
// Good - non-function property plucked, should be an error
pf<TestProps>(['a'])
// Bad - all plucked properties extend `Function`, but this is nevertheless an error
pf<TestProps>(['b', 'c'])
// We end up needing `TestProps` to not have any non-function properties whatsoever
// ...or to specify the second type argument explicitly
pf<TestProps, 'b' | 'c'>(['b', 'c'])
What's happened is that we only have one degree of freedom here. Defaulting TKey
to a type derived from TComponent
has ended up imposing an additional constraint on TComponent
instead of constraining the parameter that uses TKey
with respect to TComponent
.
I think the behavior seen above is valid, and that default type arguments are a totally different ballgame. They shouldn't be mixed up in our problem with independent inference of type parameters when instantiating abstract function types.