It is a known issue (and currently working as intended) that implicit index signatures are not added to values of an interface
type, whereas they are added for anonymous object types. See microsoft/TypeScript#15300 for discussion.
Technically speaking it is not safe to add implicit index signatures this way. Object types in TypeScript are open/extendible and not closed/exact (see microsoft/TypeScript#12936 for discussion/request for exact types), so it's always possible to have properties in addition to the known properties mentioned in the interface:
const someObject = {
fooIProp: "foo",
barIProp: "bar",
baz: 12345
};
const unexpectedMyInterface: MyInterface = someObject; // no error
(although if you try to add such unknown properties directly in an annotated object literal you will get excess property warnings, which sometimes makes object types in TypeScript seem exact when they are not).
The fact that unexpectedMyInterface
is assignable to MyInterface
but not to MyGenericInterface
is why it is technically correct to prohibit the assignment. Of course you can go through the same exercise with MyType
:
const someOtherObject = {
fooTProp: "foo",
barTProp: "bar",
baz: 12345
};
const unexpectedMyType: MyType = someOtherObject;
...yet the bad assignment of unexpectedMyType
to MyGenericInterface
is allowed:
const badMyGenericInterface: MyGenericInterface = unexpectedMyType;
console.log(badMyGenericInterface.baz?.toUpperCase()); // no error in TS, but
// RUNTIME ?? badMyGenericInterface.baz.toUpperCase is not a function
So, what gives? It seems to be a tradeoff. It is considered "safer" to allow this for anonymous object types and not interface types because interface types can be augmented or merged later, while anonymous object types can't be.
Personally I don't see it as much safer for one than the other. If you want to see implicit index signatures apply to interface types also, then you might want to go to microsoft/TypeScript#15300 and give it a ??.
Until and unless that feature gets implemented, all you can have is workarounds. One is to use a mapped type to convert a value of MyInterface
to an object type equivalent before assigning it to MyGenericInterface
:
type Id<T> = { [K in keyof T]: T[K] } // change interface to mapped type version
type MyInterfaceAsType = Id<MyInterface>;
const testFromInterface2: MyGenericInterface = testInterface as MyInterfaceAsType
const testInterfaceAnonymousTypeVersion: MyInterfaceAsType = testInterface;
const testFromInterface3: MyGenericInterface = testInterfaceAnonymousTypeVersion;
There may be other workarounds but I'd need to see more of a use case before spending too much time suggesting them. Where are you seeing failures you'd like to fix?
Playground link to code