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

Fall back on default type parameter when inference does not yield a more suitable type #16229

Closed
alxhub opened this issue Jun 2, 2017 · 6 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@alxhub
Copy link

alxhub commented Jun 2, 2017

TypeScript Version: 2.3.2

Code

class O<T=any> {
    constructor(public array: T[]) { }
}

declare let val: O<number> | O<string>; 
declare function f<T=any>(x: O<T>): T;
// Inference fails here.
f(val);

Actual behavior:

An inference error is produced on the f(val) invocation.

The type argument for type parameter 'T' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
Type argument candidate 'number' is not a valid type argument because it is not a supertype of candidate 'string'.

Expected behavior:

TypeScript should be able to accept this program.

There are two possible paths by which this program could be accepted.

  1. TypeScript could infer T = number | string.

In essence, the f(val) call requires TypeScript solve O<number> | O<string> = O<T>. This is solvable if TypeScript can prove that O<number> | O<string> is a subtype of O<number | string> which depends on the variance of O.

It's possible that TypeScript is already doing the right thing here and failing to infer a type for T given what it knows.

  1. TypeScript could fall back on the default type T = any when inference for T fails.

I believe this would be a valid improvement to the existing logic. Given the presence of the default type for T and the fact that val satisfies it, the given default of any should be used as inference has failed to identify a narrower type.

It is unexpected that the compiler ignores the provided default for T which would allow the program to be accepted in favor of rejecting the program because inference is unable to solve for T.

Real world example:

Angular is attempting to add generics to our Forms API in a backwards-compatible manner, using any as a default generic type for the values of composed form structures.

Here is the basic class structure:

class AbstractControl<T=any> { 
    value: T;
}
class FormGroup<T=any> extends AbstractControl<T> { 
    constructor(
        public controls: {[key in keyof T]: AbstractControl<T[key]>}
    ) {
        super()
    }
}
class FormArray<T=any> extends AbstractControl<T>{ 
    constructor(
        public controls: AbstractControl<T>[]
    ) {
        super()
    }
}
class FormControl<T=any> extends AbstractControl<T>{ 
    constructor(value: T) {
        super()
        this.value = value
    }
}

We have been unable to deploy this solution as it breaks the following existing example:

let a = new FormArray([
  new FormGroup({'c2': new FormControl('v2'), 'c3': new FormControl('v3')}),
  new FormArray([new FormControl('v4'), new FormControl('v5')])
]);

Here, the array passed to the FormArray constructor has a union type (similar to val in the theoretical example above), and inference for FormArray's generic T parameter fails. In this case, the default parameter type of any could be correctly applied and would ensure this code is still accepted (as it was before the addition of generics).

Thanks to @rkirov for his insight and the simplified theoretical example, and @Toxicable for the initial report.

@sam-s4s
Copy link

sam-s4s commented Aug 1, 2017

Ah this would be great if it got fixed!

@dannyskoog
Copy link

Any plans to fix this one soon? Cheers

@niazlake
Copy link

niazlake commented Jul 5, 2019

Hey, this issue opened 2 years ago 👎 . Nobody doesn't fix this(

@Sina7312
Copy link

So 3 years and still nothing?!

@andrewbranch andrewbranch added Working as Intended The behavior described is the intended behavior; this is not a bug and removed In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Oct 6, 2020
@andrewbranch
Copy link
Member

TypeScript could fall back on the default type T = any when inference for T fails.

This is just not what type parameter defaults are for—they very intentionally play no part in inference. I argue that you want to know when inference “fails.” Let me modify the original example very slightly:

interface Box<T> {
    get: () => T;
    set: (x: T) => void;
}

declare function doSomethingWithBox<T = any>(box: Box<T>): T;

declare let box: Box<number> | Box<string>;

doSomethingWithBox(box);

The difference here is that Box, unlike Array (which is the salient feature of O), is written to be invariant on T, as Array ought to be if we were aiming for strict soundness. Because of this change, there now exists no sound instantiation of T at doSomethingWithBox(box). In other words, if I were to explicitly provide the type argument, the only type I could write that would type check is doSomethingWithBox<any>(box). Is this an argument for falling back to any? I see it as the opposite. My call is inherently unsafe, so I should be alerted to that fact. I would be unhappy if my error were silently suppressed, potentially by a type definition I got from an @types package. If I truly want to silence the error, I can always supply the any type argument myself.

@EliseyMartynov
Copy link

EliseyMartynov commented Oct 12, 2022

Thank you for even asking that 😄 I was looking for fallbacking generics and your f<T=any> helped me :)

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

8 participants