Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't I indirectly return an object literal to satisfy an index signature return type in TypeScript?

Tags:

These three functions seem to do the same thing, but the last one is an error. Why is this the case?

interface StringMap {
    [key: string]: string;
}

function a(): StringMap {
    return { a: "1" }; // OK
}

function b(): StringMap {
    var result: StringMap = { a: "1" };
    return result; // OK
}

function c(): StringMap {
    var result = { a: "1" };
    return result; // Error - result lacks index signature, why?
}
like image 671
Ryan Cavanaugh Avatar asked Feb 27 '14 18:02

Ryan Cavanaugh


1 Answers

This behavior is going away.

Starting at whatever release comes after TypeScript 1.8 (or right now if you're using the bleeding-edge compiler), you will no longer see this error when the originating expression for a type is an object literal.

See https://github.com/Microsoft/TypeScript/pull/7029


The Old Answer for The Old Compilers

Index signatures and object literals behave specially in TypeScript. From spec section 4.5, Object Literals:

When an object literal is contextually typed by a type that includes a string index signature of type T, the resulting type of the object literal includes a string index signature with the widened form of the best common type of T and the types of the properties declared in the object literal.

What does this all mean?

Contextual Typing

Contextual typing occurs when the context of an expression gives a hint about what its type might be. For example, in this initialization:

var x: number = y;

The expression y gets a contextual type of number because it's initializing a value of that type. In this case, nothing special happens, but in other cases more interesting things will occur.

One of the most useful cases is functions:

// Error: string does not contain a function called 'ToUpper'
var x: (n: string) => void = (s) => console.log(s.ToUpper());

How did the compiler know that s was a string? If you wrote that function expression by itself, s would be of type any and there wouldn't be any error issued. But because the function was contextually typed by the type of x, the parameter s acquired the type string. Very useful!

Index Signatures

An index signature specifies the type when an object is indexed by a string or a number. Naturally, these signatures are part of type checking:

var x: { [n: string]: Car; };
var y: { [n: string]: Animal; };
x = y; // Error: Cars are not Animals, this is invalid

The lack of an index signature is also important:

var x: { [n: string]: Car; };
var y: { name: Car; };
x = y; // Error: y doesn't have an index signature that returns a Car

Hopefully it's obvious that the above two snippets ought to cause errors. Which leads us to...

Index Signatures and Contextual Typing

The problem with assuming that objects don't have index signatures is that you then have no way to initialize an object with an index signature:

var c: Car;
// Error, or not?
var x: { [n: string]: Car } = { 'mine': c };

The solution is that when an object literal is contextually typed by a type with an index signature, that index signature is added to the type of the object literal if it matches. For example:

var c: Car;
var a: Animal;
// OK
var x: { [n: string]: Car } = { 'mine': c };
// Not OK: Animal is not Car
var y: { [n: string]: Car } = { 'mine': a };

Putting It All Together

Let's look at the original functions in the question:

function a(): StringMap {
    return { a: "1" }; // OK
}

OK, because expressions in return statements are contextually typed by the return type of the function. The object literal {a: "1"} has a string value for its sole property, so the index signature can be successfully applied.

function b(): StringMap {
    var result: StringMap = { a: "1" };
    return result; // OK
}

OK, because the initializers of explicitly-typed variables are contextually typed by the type of the variable. As before, the index signature is added to the type of the object literal.

function c(): StringMap {
    var result = { a: "1" };
    return result; // Error - result lacks index signature, why?
}

Not OK, because result's type does not have an index signature.

like image 73
Ryan Cavanaugh Avatar answered Oct 13 '22 13:10

Ryan Cavanaugh