Skip to content

Inference failing for conditional types in function parameters #33369

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
squidfunk opened this issue Sep 11, 2019 · 9 comments
Closed

Inference failing for conditional types in function parameters #33369

squidfunk opened this issue Sep 11, 2019 · 9 comments
Labels
Duplicate An existing issue was already created

Comments

@squidfunk
Copy link

squidfunk commented Sep 11, 2019

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

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Sep 11, 2019

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

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Sep 11, 2019

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

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Sep 11, 2019

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

@squidfunk
Copy link
Author

squidfunk commented Sep 11, 2019

@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.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Sep 11, 2019

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.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Sep 13, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.7.0 milestone Sep 13, 2019
@weswigham
Copy link
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.

@weswigham
Copy link
Member

weswigham commented Feb 6, 2020

Additionally, even if we got past that, when we go to compare <QueryT extends Foo<string>>(foo: FooLike<QueryT>, data: string) => boolean, because of the contextual signature, we erase it down to (foo: FooLike<QueryT>, data: string) => boolean and compare against FooQueryLike<QueryT> (hoisting the type param to the outside so we can try to move it to the return type); however (foo: FooLike<QueryT>, data: string) => boolean ends up not being assignable to FooQueryLike<QueryT> because FooQueryLike<QueryT> is a generic conditional and nothing is assignable to it (this is an outstanding bug)! The case where we don't rip off the type parameter and leave it in place causes us to convert the signature to its' "canonical signature", which involves mapping all the local type params to constraints (and instantiating the target in the context of that type); consequently, that removes the generics from the equation, FooQueryLike is evaluated to a concrete type, and the assignment checks out.

@weswigham weswigham added Bug A bug in TypeScript Domain: Conditional Types The issue relates to conditional types Domain: Type Inference Related to type inference performed during signature resolution or `infer` type resolution and removed Needs Investigation This issue needs a team member to investigate its status. labels Feb 6, 2020
@weswigham
Copy link
Member

weswigham commented Feb 6, 2020

While a fix to #26933 won't also fix this, it is a prerequisite, so this is blocked on #26933 (or at least #34882).

@RyanCavanaugh RyanCavanaugh added Duplicate An existing issue was already created and removed Bug A bug in TypeScript Domain: Conditional Types The issue relates to conditional types Domain: Type Inference Related to type inference performed during signature resolution or `infer` type resolution Rescheduled This issue was previously scheduled to an earlier milestone labels Feb 14, 2023
@RyanCavanaugh
Copy link
Member

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

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Feb 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants