Typescript: Bad type inference under `--strictFunctionTypes`

Created on 30 Oct 2017  ·  6Comments  ·  Source: microsoft/TypeScript




TypeScript Version: 2.7.0-dev.20171029

Code

type Comparator<T> = (x: T, y: T) => number;

interface LinkedList<T> {
    comparator: Comparator<T>,
    nodes: Node<T>
}

type Node<T> = { value: T, next: Node<T> } | null

declare function compareNumbers(x: number, y: number): number;
declare function mkList<T>(items: T[], comparator: Comparator<T>): LinkedList<T>;

const list: LinkedList<number> = mkList([], compareNumbers); // `T` inferred as `never`,
                                                             //     should   be `number`

Actual behavior:

The implicit evolving never[] type of the array literal is taking precedence, leading to unexpected results.

Fixed Suggestion

Most helpful comment

One option here is to make no inferences form a fresh array literal. the parallel in an object literal case here would be:

declare function mkList<T>(items: { 0?: T }, comparator: Comparator<T>): LinkedList<T>;
mkList({}, compareNumbers); // `number`, since no inferences are made from `{}`

All 6 comments

I think this is an intended behavior. compareNumbers type argument is covariant. This means number must be a supertype of T. Empty array is typed initially as never, a subtype of every other type. So, type checker should choose never, so number is correctly typed covariantly.

I would suggest annotate mkList<number>

That does feel like never is infecting the inference though. While it might be strictly _sound_ it feels like resolution should only assign never when there is no other suitable type.

I think the main problem is that an unresolved contextually typed literal takes precedence. The array is not a variable reference, it's a literal. What surfaces is the presumably evolving implicit never[] type, which is unexpected because it trumps user provided annotations.

One option here is to make no inferences form a fresh array literal. the parallel in an object literal case here would be:

declare function mkList<T>(items: { 0?: T }, comparator: Comparator<T>): LinkedList<T>;
mkList({}, compareNumbers); // `number`, since no inferences are made from `{}`

Yes, I think that would be the best approach, but only if the array is empty.

Example of breaking soundness with an aliased never[]

type Comparator<T> = (x: T, y: T) => number;

declare const compareStrings: Comparator<string>;
declare const compareNumber: Comparator<number>;

function makeArray<T1, T2>(arr1: T1[], arr2: T2[], c1: Comparator<T1>, c2: Comparator<T2>) {
    return {
        arr1,
        arr2
    }
}

const arr: never[] = [];
const res = makeArray(arr, arr, compareStrings, compareNumber);

res.arr1.push("");
res.arr2.push(10);
Was this page helpful?
0 / 5 - 0 ratings