Skip to content

Inference failing for conditional types in function parameters #33369

Description

When using generics with conditionals in a function's signature, type inference seems to break down. I narrowed the problem down to the following reproducible case. Interestingly, the issues vanishes when another generic "helper" parameter is introduced, as can be seen in the example.

TypeScript Version: 3.6.3, 3.6.2, 3.5.x

Search Terms: parameter, inference, functions, generic, conditional types

Code

export interface Foo<T> {
  bar: T
}

export type FooType<T> = T extends Foo<infer U> ? U : never
export type FooLike<T> = T extends Foo<FooType<T>> ? T : never

export type FooQueryType<T> =
  T extends (foo: infer U, ...args: any[]) => boolean
    ? U extends FooLike<infer V>
      ? V
      : never
    : never

export type FooQueryParameters<T> =
  T extends (foo: any, ...args: infer U) => boolean
    ? U
    : never

export type FooQueryLike<T> =
  T extends (foo: FooQueryType<T>, ...args: any[]) => boolean
    ? T
    : never

export type FooQuery<T> = (foo: FooLike<T>) => boolean

export function matchWithBrokenInference<T>(
  fn: FooQueryLike<T>, ...args: FooQueryParameters<T>
): FooQuery<FooQueryType<T>> {
  return foo => fn(foo, ...args)
}

export function matchWithCorrectInference<T, U extends FooQueryLike<T>>(
  fn: T, ...args: FooQueryParameters<U>
): FooQuery<FooQueryType<U>> {
  return foo => (fn as U)(foo, ...args)
}

function query<T extends Foo<string>>(foo: FooLike<T>, data: string) {
  return true
}

const q1 = matchWithBrokenInference(query, "test") // incorrect inference of query
const q2 = matchWithCorrectInference(query, "test")

Expected behavior:

matchWithBrokenInference infers query correctly.

Actual behavior:

matchWithBrokenInference infers query incorrectly.

Playground Link: reproducible example

Related Issues: None found

Activity

  1. AnyhowStep commented on Sep 11, 2019

    @AnyhowStep
    Contributor

    This type,

    type FooQueryLike<T> =
      T extends (foo: FooQueryType<T>, ...args: any[]) => boolean
        ? T
        : never

    And your usage of it are very suspicious.


    I've also noticed you don't constrain your type parameters and choose to leave them implicitly unknown.

    You should constrain them as much as possible.


    For example, this,

    export type FooType<T> = T extends Foo<infer U> ? U : never

    Can be rewritten as,

    export type FooType<T extends Foo<any>> = T["bar"]

    Saving you the need to use a conditional type. Conditional types should really be more of a last resort.


    I'm on mobile now so I can't poke at it more.

    Upon a little more inspection, your types can be greatly simplified but I'm not going to do that on my phone =x

  2. AnyhowStep commented on Sep 11, 2019

    @AnyhowStep
    Contributor

    Okay, I tried to simplify on mobile, lol,

    export interface Foo<T=any> {
      bar: T
    }
    
    type Query = (foo: Foo, ...args: any[]) => boolean;
    
    export type QueryFoo<T extends Query> =
      T extends (foo: infer U, ...args: any[]) => boolean ?
        U :
        never
    
    export type QueryParameters<T extends Query> =
      T extends (foo: any, ...args: infer U) => boolean
        ? U
        : never
    
    export type Result<T extends Foo> = (foo: T) => boolean
    
    declare function match<T extends Query> (query : T, ...args : QueryParameters<T>) : (
      Result<QueryFoo<T>>
    );

    [Edit]
    Removed unnecessary type param from Query

  3. AnyhowStep commented on Sep 11, 2019

    @AnyhowStep
    Contributor

    I'm even tempted to say this should work,

    export interface Foo<T=any> {
      bar: T
    }
    
    export type Result<FooT extends Foo> = (foo: FooT) => boolean
    
    declare function match<
      FooT extends Foo,
      ParamsT extends readonly any[]
    > (
      query : (foo : FooT, ...args : ParamsT) => boolean, 
      ...args : ParamsT
    ) : (
      Result<FooT>
    );

    However, I'm on mobile and can't test that

  4. squidfunk commented on Sep 11, 2019

    @squidfunk
    Author

    AnyhowStep thanks for your input on this. I agree with you in general, but actually, the FooLike interface which I defined is necessary in my case, as I have interfaces extending from Foo. The conditional types help preserve the extended interface type, which means that the type inference will correctly infer the extended type of Foo when using FooLike and not Foo. Also, the generic type parameter of Foo (which you defaulted to any) is necessary in my case.

  5. AnyhowStep commented on Sep 11, 2019

    @AnyhowStep
    Contributor

    I just tested and the type of foo is preserved,

    export interface Foo<T=any> {
      bar: T
    }
    
    export type Result<FooT extends Foo> = (foo: FooT) => boolean
    
    declare function match<
      FooT extends Foo,
      ParamsT extends readonly any[]
    > (
      query : (foo : FooT, ...args : ParamsT) => boolean, 
      ...args : ParamsT
    ) : (
      Result<FooT>
    );
    
    interface MyFooLike {
        bar : "haha",
        baz : "hehe",
    }
    declare function myQuery (
        foo : MyFooLike,
        arg0 : number,
        arg1 : Date
    ) : boolean;
    
    //const result: Result<MyFooLike>
    const result = match(myQuery, 3.141, new Date());
    //type resultParams = [MyFooLike]
    type resultParams = Parameters<typeof result>;

    Playground

    You can see myQuery has MyFooLike and the result also has MyFooLike.

    MyFooLike extends Foo and is still inferred and preserved.

  6. weswigham commented on Feb 6, 2020

    @weswigham
    Member

    OK, so the issue is due to contextual typing; specifically recent additions around generic signatures. The contextual type of query in the functioning case is T - T is unconstrained and has no signatures, which means we forward along the type of query unmodified into inference (which is then immediately matched against T). In the "broken" case, the argument type is FooQueryLike<T>, which does have signatures, which causes us to go "ah, we may need to unify the type parameters in query with the arguments of FooQueryLike. Let's defer inference to them until the next inference stage". We then produce unknown as the inference result for T during that first stage, since we made no successful inferences. Before moving onto the next stage, we check that none of our inferences have already made the signature invalid, but, whatdoyouknow, that unknown (from having made no inferences whatsoever), when fed through the arguments, becomes never for the first argument and triggers an error, causing us to bail on the second inference phase and immediately issue an error. Generally speaking we assume that when a type parameter is instantiated with it's constraint, a call should succeed, as the constraint it supposedly strictly "broader" than the type parameter itself; but conditionals throw a massive wrench into this, as the constraint of an unconstrained type parameter isn't going to extend anything in a conditional, which means those conditions are always going to evaluate to their false branches, which, more often than not, will produce a never which in turn disallows any actual assignments.

  7. 25 remaining items

  8. added
    DuplicateAn existing issue was already created
    and removed
    BugA bug in TypeScript
    Domain: check: Type InferenceRelated to type inference performed during signature resolution or `infer` type resolution
    RescheduledThis issue was previously scheduled to an earlier milestone
    on Feb 14, 2023
  9. RyanCavanaugh commented on Feb 14, 2023

    @RyanCavanaugh
    Member

    Based on the above explanation, I'm going to track this as a duplicate of #26933

  10. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions