39

I just noticed that an interface cannot be assigned to Record<string, unknown> (playground link):

interface Foo {
    foo: number
}

const foo: Foo = { foo: 1 }
const bar: Record<string, unknown> = foo
//    |-> Error: Type 'Foo' is not assignable to type 'Record<string, unknown>'
//               Index signature is missing in type 'Foo'.(2322)

However the same is possible, when the type declaration for Foo is omitted (playground link):

const foo = { foo: 1 }
const bar: Record<string, unknown> = foo // no error here

Question: Why is there a difference between both examples? For me the reduced type of the variable foo is the same in both examples... Shouldn't the interface Foo be assignable to Record<string, unknwon>?!

In my understanding Record<string, unknown> is equivalent to object and thus any interface should be assignable to it. Also https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md suggests using Record<string, unknown> instead of object.

Remarks: The first example works when either object (playground link) or Record<string, any> (playground link) is used instead of Record<string, unknown>.

Stephan Kulla
  • 4,739
  • 3
  • 26
  • 35

2 Answers2

36

You've run across Index signature is missing in type (only on interfaces, not on type alias) #15300

The code will work when you change an interface to a type:

type Foo = {
    foo: number
}

const foo: Foo = { foo: 1 };
const bar: Record<string, unknown> = foo;
Lesiak
  • 22,088
  • 2
  • 41
  • 65
4

EDIT: @Lesiak has the correct answer above. I'm leaving this solely for the link to the related answer.

Admittedly, I'm a bit out of my depth here, but I'm looking through this answer and I see:

[A] big part of TypeScript's safety comes from the fact that [...] it'll only let you treat an object as a dictionary if it knows it's explicitly intended as one.

Which is consistent with my testing. Modifying your interface to explicitly treat Foo.foo as an index does not produce the error. (playground link)

interface Foo {
    [foo: string]: number
}

const foo = { foo: 1 }
const bar: Record<string, unknown> = foo

This doesn't answer your question fully, as Record<string, any> works with your explicit interface, but maybe someone more knowledgeable can take it from here.

D M
  • 5,769
  • 4
  • 12
  • 27
  • 2
    Actually `interface Foo { foo: number, [key: string]: unknown }` should be the right solution [playground link](https://www.typescriptlang.org/play?ssl=1&ssc=1&pln=6&pc=1#code/JYOwLgpgTgZghgYwgAgGIHt3IN4ChkHIyYBcyIArgLYBG0+hA2gNYQCeZAzmFKAOYBdMhRDMQ6AO4hcAX1y4E6ENyKk0mZAF4cq9GQCMyOYuVhkNOFDIAlCIqgATADzdeIPgBpkIsZJAA+LV0gA) since it means: "an object with a property `foo` of type `number` and it can be used as a dictionary" – Stephan Kulla Jan 19 '21 at 22:46
  • 1
    In the linked thread there is a question why `Record` from just 5 days ago: https://github.com/microsoft/TypeScript/issues/15300#issuecomment-760165845 :-) I also wonder why this works... – Stephan Kulla Jan 19 '21 at 22:49