John John - 1 month ago 5
TypeScript Question

Type inference success depends on naming a value

I'm still new to TypeScript and frequently find things I find really odd.

Take this code:

interface MyInterface<T> {
[i: string]: T;
}

function myFunc<T>(arg: MyInterface<T>) { }

var o = { 'val1': 42, 'val2': 43 };

myFunc(o);


The call fails with the complaint that the "Index signature is missing".

Interestingly, it's not missing when I write

myFunc({ 'val1': 42, 'val2': 43 });


That would mean that the expression
{ 'val1': 42, 'val2': 43 }
's type is inferred backwards from the call (which is somewhat expected), but not so when bound to a name (which is weird and not how I know backward inference to work from F#, for example).

But it gets even stranger.

If I change the interface to

interface MyInterface<T> {
[i: number]: T;
}


then the corresponding

var o = [42, 43];

myFunc(o);


also works (in addition to passing the array directly).

Is that by design? What are the rules that explain this?

Answer

The reason you're seeing this inconsistency is because you're using TypeScript 1.8. If you try either out in the TypeScript playground, you'll see either one works.

The reason for this behavior in TS 1.8 is because when you placed the object literal directly in the call to myFunc, it was contextually typed by arg, whose type has an index signature. As a result, the inferred type gained an index signature.

Contextual typing doesn't modify a variable's prior inferred type (the declared type), so when you tried to pass o to myFunc, you got an error that o was missing an index signature.

In TypeScript 2.0 (and above), these scenarios were painful enough that we brought in the concept of implicit index signatures. Check out the pull request here: https://github.com/Microsoft/TypeScript/pull/7029.

To quote:

With this PR an object literal type is assignable to a type with an index signature if all known properties in the object literal are assignable to that index signature. This makes it possible to pass a variable that was initialized with an object literal as parameter to a function that expects a map or dictionary:

function httpService(path: string, headers: { [x: string]: string }) { }

const headers = {
    "Content-Type": "application/x-www-form-urlencoded"
};

httpService("", { "Content-Type": "application/x-www-form-urlencoded" });  // Ok
httpService("", headers);  // Now ok, previously wasn't