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

Generic type of varargs is inferred from only the first parameter #51273

Closed
gomain opened this issue Oct 22, 2022 · 7 comments
Closed

Generic type of varargs is inferred from only the first parameter #51273

gomain opened this issue Oct 22, 2022 · 7 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@gomain
Copy link

gomain commented Oct 22, 2022

Bug Report

🔎 Search Terms

  • varargs
  • generic

🕗 Version & Regression Information

This is a crash

No

This changed between versions ______ and _______

All version in the playground error. Versions 3.62 and earlier don't give meaningful error, i.e. just Errors in code.

This is the behavior in every version I tried, and I reviewed the FAQ for entries about ________

Yes. And there is no FAQ on this subject.

I was unable to test this on prior versions because _______

All version in the playground have been tested. Including Nightly.

⏯ Playground Link

Playground link with relevant code

💻 Code

type A = { a: any };
type B = { b: any };

declare const a: A;
declare const b: B;

function takeVarArgsOfGeneric<ARG>(...args: Array<ARG>) {
  console.log(args);
}
// error, infers ARG from only the first position
takeVarArgsOfGeneric(a, b);
// but ok when spread
takeVarArgsOfGeneric(...[a, b]);

🙁 Actual behavior

Type error:

Argument of type 'B' is not assignable to parameter of type 'A'.
  Property 'a' is missing in type 'B' but required in type 'A'.

Inferred type:

function takeVarArgsOfGeneric<A>(...args: A[]): void

🙂 Expected behavior

Inferred type:

function takeVarArgsOfGeneric<A | B>(...args: (A | B)[]): void
@MartinJohns
Copy link
Contributor

Pretty sure it's a duplicate of #37673.

@gomain
Copy link
Author

gomain commented Oct 24, 2022

Discussion in #37673 concludes that this is working as intended. Argument being that it is the same behaviour as a non-varargs variant.

// same issue when positioned
function takeMultipleGeneric<ARG>(first: ARG, second: ARG) {
  console.log(...arguments);
}
takeMultipleGeneric(a, b);

But doesn't go further into explaining why this would be the desired behaviour, if there is one it should make it into the FAQ.

Here I suggest it is not the desired behaviour both in the varags/rest and the multiple positions cases.

This is not exactly the same case though. When using the rest syntax, the programmer gets to explicitly name and type the rest parameter as an Array of something and would expect the type system to resolve types in the same way arrays are resolved. I.e.

function takeArrayOfGeneric<ARG>(args: Array<ARG>) { }

In the case of positioned parameters, this would be expecting typescript implicitly resolves the type of arguments which by specification is truly dynamic and rightfully have type being the equivalence of ArrayLike<any>.

It should, however, be expected that the named positional parameters types are resolved as unions.

Is this a case of lacking unification #30134?

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Oct 25, 2022

Inference collects some number of candidates and then decides what to do with those candidates. In the case where 2 or more candidates are collected but no common type is found, it falls back to one of them and tries to process the call anyway. In the case where 1 candidate is collected, that's obviously the type argument's value.

Automatically inferring a union with two different types are present means that generic inference can effectively never fail; this was effectively the behavior prior to TS 1.6 and we got bugs basically every day saying that this makes generics useless. A thought experiment of "What calls would be allowed if this were the case" quickly reveals how undesirable this would be as a default behavior.

So the question is whether a spreaded array should act like the code that collects 2 arguments or 1, and you can make a consistency-based argument in either direction:

// Direct call: Collects two candidates (A and B).
// Union is not automatically inferred from two disparate candidates (intended)
takeVarArgsOfGeneric(a, b);

// At issue: middle case behaves as the one above, or the one below?
takeVarArgsOfGeneric(...[a, b]);

// Indirected type: (A | B)[]
const arr = [a, b];
// Only one candidate is collected (A | B)
takeVarArgsOfGeneric(...arr);
// is same as
takeVarArgsOfGeneric<A | B>(...arr);

if there is one it should make it into the FAQ.

This isn't really frequently asked FWIW

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Oct 25, 2022
@gomain
Copy link
Author

gomain commented Oct 26, 2022

@RyanCavanaugh Thanks for explaining.

To recap. When you declare a function as

function takeTwoParametersOfType<T>(first: T, second: T) { ... }

you want the type system to enforce that first and second are indeed the same type, and fail otherwise. Had they resolved as unions, things like

function max<T>(first: T, second: T): T { ... }

would be useless.

On the other hand, if you declare

function takeSomeParametersOfType<T>(...params: Array<T>) { ... }

you get same type enforcement when called using positioned variant (multiple candidates provided).

takeSomeParametersOfType(a, b); // Argument of type 'B' is not assignable to parameter of type 'A'

but don't when called using the spread variant as the type of the array (the single candidate) is resolved prior to resolving T.

takeSomeParametersOfType(...[a, b]); // ok, T is A | B

@TFTomSun
Copy link

TFTomSun commented Jan 21, 2023

I have a similar use case, I try to infer a generic argument from an generic container array.`Both discussed ways don't work in that case.

TS Playground

class ValueContainer<T>{
  public constructor (public value:T){
    
  }
}

declare const a: ValueContainer<number>;
declare const b: ValueContainer<string>;

function takeVarArgsOfGeneric<ARG>(...args: Array<ValueContainer<ARG>>) {
  return args[0].value;
}
// error: ValueContainer<string>  is not assignable to ValueContainer<number>
var result1 = takeVarArgsOfGeneric(a, b);
// result 1 expected to be number | string
// error: ValueContainer<number> | ValueContainer<string>  is not assignable to ValueContainer<number>
var result2 = takeVarArgsOfGeneric(...[a, b]);
// result 2 expected to be number | string

@RyanCavanaugh Is it somehow possible to solve that by giving Typescript more information about the generic structure?

@RyanCavanaugh
Copy link
Member

Yes

type Helper1<T> = T extends { [n: number]: infer X } ? Helper2<X> : never;
type Helper2<T> = T extends ValueContainer<infer U> ? U : never;
function takeVarArgsOfGeneric<ARG>(...args: ARG & Array<ValueContainer<any>>): Helper1<ARG> {

@github-actions
Copy link

github-actions bot commented Jun 8, 2023

This issue has been marked as 'Not a Defect' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

4 participants