Skip to content
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

Inference failing for conditional types in function parameters #33369

Open
squidfunk opened this issue Sep 11, 2019 · 5 comments

Comments

@squidfunk
Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants
You can’t perform that action at this time.