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

Array cannot infer multiple types with functions #31617

Closed
Domino9697 opened this issue May 28, 2019 · 6 comments
Closed

Array cannot infer multiple types with functions #31617

Domino9697 opened this issue May 28, 2019 · 6 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@Domino9697
Copy link
Contributor

TypeScript Version: 3.4.5 & 3.5.0-dev.20190525

Search Terms:
Array infer function generic types
Code

type ArgType<T> = {(): T}

type ArgTypes<T> = ArgType<T> | ArgType<T>[]

interface Test {
  test<T>(args: ArgTypes<T>): void
}

declare const Test: Test;

Test.test([() => "hello", () => 4])

Expected behavior:

I expect no errors and T should be inferred to string | number.

Actual behavior:

An error appears in the console:

Argument of type '((() => string) | (() => number))[]' is not assignable to parameter of type 'ArgTypes<string>'.
  Type '((() => string) | (() => number))[]' is not assignable to type 'ArgType<string>[]'.
    Type '(() => string) | (() => number)' is not assignable to type 'ArgType<string>'.
      Type '() => number' is not assignable to type 'ArgType<string>'.
        Type 'number' is not assignable to type 'string'.

Playground Link:

https://www.typescriptlang.org/play/#src=type%20ArgType%3CT%3E%20%3D%20%7B()%3A%20T%7D%0D%0A%0D%0Atype%20ArgTypes%3CT%3E%20%3D%20ArgType%3CT%3E%20%7C%20ArgType%3CT%3E%5B%5D%0D%0A%0D%0Ainterface%20Test%20%7B%0D%0A%20%20test%3CT%3E(args%3A%20ArgTypes%3CT%3E)%3A%20void%0D%0A%7D%0D%0A%0D%0Adeclare%20const%20Test%3A%20Test%3B%0D%0A%0D%0ATest.test(%5B()%20%3D%3E%20%22hello%22%2C%20()%20%3D%3E%204%5D)

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label May 28, 2019
@RyanCavanaugh
Copy link
Member

This is an intentional trade-off so that generics can catch errors where you expect multiple objects to be of the same type, which is usually more common.

It doesn't seem like your function needs to be generic at all (it could just be test(args: ArgTypes<unknown>), or you could specify Test.test<string | number>(.... If you have a more concrete use case I can advise on other ways to write the signature to accomplish what you're looking for.

@Domino9697
Copy link
Contributor Author

Thank you for your answer @RyanCavanaugh ! I just provided a minimal reproduction of the issue.
I was actually working on Vue TypeScript and I need a generic function to infer the types of my component prop (to be used elsewhere).

I see your point but I did some more testing and I do not understand why my code above is not working while this code is:

type ArgType<T> = { (): T }

type ArgTypes<T> = ArgType<T> | ArgType<T>[]

type ArgDefinition<T> = {
  [K in keyof T]: ArgTypes<T[K]>
}

interface Test {
  test<T>(args: ArgDefinition<T>): void
}

declare const Test: Test;

Test.test({a: [() => "hello", () => 4]})

And T is correctly inferred as {a: string | number}.

@RyanCavanaugh
Copy link
Member

It's a matter of whether inference collected multiple candidates or not. The OP has two inference sites, whereas the second example only has one because there's an object property "in the way".

@Domino9697
Copy link
Contributor Author

Thank you @RyanCavanaugh for the explanation !

@nemosmithasf
Copy link

nemosmithasf commented May 3, 2021

It's a matter of whether inference collected multiple candidates or not. The OP has two inference sites, whereas the second example only has one because there's an object property "in the way".

@RyanCavanaugh, is there any documentation on the rules behind TS inferences? I've been playing around with the examples provided here for a while now, and I'm still not sure I fully understand what TS considers "multiple inference sites" (i.e. is it simply a case of objects props have its own inference context or is it more nuanced?)

@xavdid
Copy link

xavdid commented May 14, 2021

@RyanCavanaugh re: concrete use case (that I believe ties to this), we're trying to take a function and curry it to remove required properties. Something like:

// https://stackoverflow.com/a/61108377/1825390
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>

interface Payload {
  a: number,
  b: number,
  c: number
}
const emit = (payload: Payload) => null

const addA = (payload: Optional<Payload, 'a'>): Payload => ({ a: 1, ...payload })
const addB = (payload: Optional<Payload, 'b'>): Payload => ({ b: 1, ...payload })

const wrappedEmit = (...funcs) => {
  return (partialPayload) => {
    const payload = funcs.reduce(...)
    return emit(payload)
  }
}

const emitWithOnlyC = wrappedEmit(addA, addB)

// only c is required in the payload object now
emitWithOnlyC({c: 1})

I've got a more complete playground example as well. Here's our working-but-too-manual real world example too, if that's helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants