Skip to content

Granular inference for generic function type arguments #20122

Closed
@masaeedu

Description

@masaeedu

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions