Typescript: Fall back on default type parameter when inference does not yield a more suitable type

Created on 2 Jun 2017  Â·  5Comments  Â·  Source: microsoft/TypeScript

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.

2) 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.

Working as Intended

Most helpful comment

Any plans to fix this one soon? Cheers

All 5 comments

Ah this would be great if it got fixed!

Any plans to fix this one soon? Cheers

Hey, this issue opened 2 years ago :-1: . Nobody doesn't fix this(

So 3 years and still nothing?!

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

siddjain picture siddjain  Â·  3Comments

seanzer picture seanzer  Â·  3Comments

Zlatkovsky picture Zlatkovsky  Â·  3Comments

manekinekko picture manekinekko  Â·  3Comments

Antony-Jones picture Antony-Jones  Â·  3Comments