187

I want MyInterface.dic to be like a dictionary name: value, and I define it as follows:

interface MyInterface {
 dic: { [name: string]: number }
}

Now I create a function which waits for my type:

function foo(a: MyInterface) {
 ...
}

And the input:

let o = {
 dic: {
 'a': 3,
 'b': 5
 }
}

I'm expecting foo(o) to be correct, but the compiler is falling:

foo(o) // TypeScript error: Index signature is missing in type { 'a': number, 'b': number }

I know there is a possible casting: let o: MyInterface = { ... } which do the trick, but why is TypeScript not recognizing my type?


Extra: works fine if o is declared inline:

foo({
 dic: {
 'a': 3,
 'b': 5
 }
})
Peter Mortensen
31.5k22 gold badges110 silver badges134 bronze badges
asked May 3, 2016 at 13:45
3
  • I am looking for answers to this one: github.com/DefinitelyTyped/DefinitelyTyped/issues/24469 Commented Mar 23, 2018 at 0:41
  • 2
    It seems as of now this exact code passes in Typescript without any issues: typescriptlang.org/play?#code/… Commented Jan 12, 2022 at 1:22
  • 2
    The same issue can still occur in other scenarios like the one described in this TS issue. As suggested by several people in this thread, the solution is to use type rather than interface in that case, because interfaces require by design explicit index signatures in those cases. Commented May 3, 2023 at 13:08

13 Answers 13

146

The problem is that when the type is inferred, then the type of o is:

{ dic: { a: number, b: number } }

That's not the same as { dic: { [name: string]: number } }. Critically, with the top signature you're not allowed to do something like o.dic['x'] = 1. With the 2nd signature you are.

They are equivalent types at runtime (indeed, they're the exact same value), but a big part of TypeScript's safety comes from the fact that these aren't the same, and that it'll only let you treat an object as a dictionary if it knows it's explicitly intended as one. This is what stops you accidentally reading and writing totally non-existent properties on objects.

The solution is to ensure TypeScript knows that it's intended as a dictionary. That means:

  • Explicitly providing a type somewhere that tells it it's a dictionary:

    let o: MyInterface

  • Asserting it to be a dictionary inline:

    let o = { dic: <{ [name: string]: number }> { 'a': 1, 'b': 2 } }

  • Ensuring it's the initial type that TypeScript infers for you:

    foo({ dic: { 'a': 1, 'b': 2 } })

If there's a case where TypeScript thinks it's a normal object with just two properties, and then you try to use it later as a dictionary, it'll be unhappy.

answered May 3, 2016 at 13:52

10 Comments

Good catch, sorry, that should've been index syntax (o.dic['x'] = 1). Example updated.
Update! TypeScript 2 should now automatically do this conversion for you, if it's valid: github.com/Microsoft/TypeScript/wiki/…
This is so stupid, can't believe Microsoft botched this, if X is a subset of Y it should be easy to cast X as Y without hassle. Right now TS can't cast [key: string]: string | number but it can cast [key: string]: any despite an interface only having strings and numbers
Spread operator helps in my case - I don't need to modify the object, so bar({ ...hash }) works for me.
As of today, it seems to work, so long as you use types, rather than interfaces. See @Nikolay's answer.
|
88

In my case, it was just necessary to use type instead of interface.

Peter Mortensen
31.5k22 gold badges110 silver badges134 bronze badges
answered Mar 8, 2022 at 11:29

3 Comments

Same for me, but why is this?
See github.com/microsoft/TypeScript/issues/15300. By design interfaces and types behave differently when it comes to index signatures.
This is true for class too. Changing to type did the job.
49

TS wants us to define the type of the index. For example, to tell the compiler that you can index the object with any string, e.g. myObj['anyString'], change:

interface MyInterface {
 myVal: string;
}

to:

interface MyInterface {
 [key: string]: string;
 myVal: string;
}

And you can now store any string value on any string index:

x['myVal'] = 'hello world'
x['any other string'] = 'any other string'

Playground Link.

answered Dec 28, 2020 at 5:07

3 Comments

This is the way to go if you don't want to loose myVal typing + autocomplete feature
but you lose type safety when you do that. myInterface was only meant to have myVal property, now it can have any property.
Yes, this question was asked by a person who wanted to be able to store some value on an arbitrary index. I edited for clarity.
48

For me, the error was solved by using type instead of interface.

This error can occur when function foo has type instead of interface for the typing parameter, like:

type MyType = {
 dic: { [name: string]: number }
}
function foo(a: MyType) {}

But the passed value typed with interface like

interface MyInterface {
 dic: { [name: string]: number }
}
const o: MyInterface = {
 dic: {
 'a': 3,
 'b': 5
 }
}
foo(o) // type error here

I just used

const o: MyType = {
 dic: {
 'a': 3,
 'b': 5
 }
}
foo(o) // It works
Ryan Taylor
13.6k3 gold badges41 silver badges34 bronze badges
answered Jan 2, 2021 at 17:16

1 Comment

Can you explain what type error you get and why using type over interface should help? I cannot reproduce what you claim: this Stackblitz example works as expected. Maybe your answer is outdated (I'm using typescript 4.3.5)
37

You can solve this problem by doing foo({...o}) playground

answered Apr 22, 2021 at 18:40

1 Comment

This is not much more than a link-only answer. Can you elaborate? (But ******* without ******* "Edit:", "Update:", or similar - the answer should appear as if it was written today.)
24

Here are my two cents:

type Copy<T> = { [K in keyof T]: T[K] }
genericFunc<SomeType>() // No index signature
genericFunc<Copy<SomeType>>() // No error
Peter Mortensen
31.5k22 gold badges110 silver badges134 bronze badges
answered Aug 20, 2021 at 18:56

Comments

15

The problem is a bit wider than OP's question.

For example, let's define an interface and variable of the interface

interface IObj {
 prop: string;
}
const obj: IObj = { prop: 'string' };

Can we assign obj to type Record<string, string>?

The answer is No. Demo

// TS2322: Type 'IObj' is not assignable to type 'Record<string, string>'. Index signature for type 'string' is missing in type 'IObj'.
const record: Record<string, string> = obj; 

Why this is happening? To describe it let's refresh our understanding of "upcasting" and "downcasting" terms, and what is the meaning of "L" letter in SOLID principles.

The following examples work without errors because we assign the "wider" type to the more strict type.

Demo

const initialObj = {
 title: 'title',
 value: 42,
};
interface IObj {
 title: string;
}
const obj: IObj = initialObj; // No error here
obj.title;
obj.value; // Property 'value' does not exist on type 'IObj'.(2339)

IObj requires only one prop so the assignment is correct.

The same works for Type. Demo

const initialObj = {
 title: 'title',
 value: 42,
};
type TObj = {
 title: string;
}
const obj: TObj = initialObj; // No error here
obj.title;
obj.value; // Property 'value' does not exist on type 'TObj'.(2339)

The last two examples work without errors because of "upcasting". It means that we cast a value type to the "upper" type, to the entity type which can be an ancestor. In other words, we can assign Dog to Animal but can not assign Animal to Dog (See meaning of "L" letter in SOLID principles). Assigning the Dog to the Animal is "upcasting" and this is safe operation.

Record<string, string> is much wider than the object with just one property. It can have any other properties.

const fn = (record: Record<string, string>) => {
 record.value1;
 record.value2;
 record.value3; // No errors here
}

That's why when you assign the IObj Interface to Record<string, string> you get an Error. You assign it to the type that extends IObj. Record<string, string> type can be a descendant of IObj.

In other answers, it is mentioned that using of Type can fix the problem. But I believe it is wrong behavior and we should avoid of using it.

Example:

type TObj = {
 title: string;
}
const obj: TObj = {
 title: 'title',
};
const fn = (record: Record<string, string>) => {
 record.value1;
 record.value2;
 // No errors here because according to types any string property is correct
 // UPD:
 // FYI: TS has a flag `noUncheckedIndexedAccess` which changes this behavior so every prop becomes optional
 record.value3; 
}
fn(obj); // No error here but it has to be here because of downcasting

The last example with the comparison of Type and Interface.

P.S.

Take a look at this issue with related question, and interesting comment.

answered Nov 16, 2022 at 21:34

Comments

5

This error is valid. You should write something like the options below:

const o = Object.freeze({dic: {'a': 3, 'b': 5}})
const o = {dic: {'a': 3, 'b': 5}} as const
const o: MyInterface = {dic: {'a': 3, 'b': 5}}

Why?

The TypeScript compiler cannot assume that o won't change between the time it is initialized and the time foo(o) is called.

Maybe somewhere in your code something like the snippet below is written:

delete o.dic

That's why the inline version works. In this case there isn't any possible update.

answered May 23, 2021 at 12:47

Comments

4

The following simple trick might be useful too:

type ConvertInterfaceToDict<T> = {
 [K in keyof T]: T[K];
};

This conversion helped me to fix the issue:

Argument of type 'QueryParameters' is not assignable to parameter of type 'Record<string, string>

Where QueryParameters is an Interface. I wasn't able to modify it directly because it comes from a third party package.

answered Sep 30, 2022 at 19:38

Comments

2

This seems to be the top result for the search term. If you already have an index type with (string, string) and you can't change type of that input, then you can also do this:

foo({...o}) // Magic

For your question, another way to do it is:

interface MyInterface {
 [name: string]: number
}
function foo(a: MyInterface) {
 ...
}
let o = {
 'a': 3,
 'b': 5
}
foo(o);
Peter Mortensen
31.5k22 gold badges110 silver badges134 bronze badges
answered Aug 11, 2022 at 5:17

Comments

2

While the error does not seem to occur in our specific example anymore, there are cases where this still occurs:

function foo(a: { [name: string]: number }) {
}
interface Test {
 test: number;
}
const test: Test = { test: 1 };
foo(test); // Error

I think the main reason for the error is that an interface can be augmented through declaration merging, so there is no guarantee that no other properties have been added to the interface that do not actually match the number type. This also means that when foo accepts { [name: string]: any }, the error disappears (the error message suggests that the error is about the key, but in fact it is about the value).

As already mentioned across the other answers and in TypeScript issue #15300, the quick fixes for the problem are:

  • Use type Test instead of interface Test – This prevents declaration merging, so the problem disappears
  • Use const test: Pick<Test, keyof Test>
  • Use foo({ ...test })

None of these solutions are universal. For example, I have a case where a third-party library defines nested interfaces, so I cannot apply any of the above approaches.

As a universal solution, I have used this helper type:

type InterfaceToType<T> = {
 [K in keyof T]: InterfaceToType<T[K]>;
}
const test: InterfaceToType<Test> = { test: 1 };
foo(test);
answered May 7, 2024 at 10:05

1 Comment

Thank you for reply and a link to declaration merging. A was able to cast type User = {name: string; email: string} to Record<string, string> with addition of [x: string]: string to User type. Never seen this error before, maybe a transitive Angular issue.
0

If switching from interface to type was not helped, here enforced solution with type, which is working for me:

type MyInterface {
 dic: { [name: string]: number } | {};
}
function foo(a: MyInterface) {
 ...
}
let o = {
 dic: {
 'a': 3,
 'b': 5
 }
}
foo(o);
answered Aug 30, 2023 at 13:32

Comments

0

Solution: use Type instead of Interface

Use case:

interface GenericMap {
 [key: string]: SomeType;
}
export class MyClass<SpecificMap extends GenericMap = GenericMap> {
 private readonly map = {} as SpecificMap;
 constructor(keys: (keyof SpecificMap)[]) {
 for (const key of keys) {
 this.statusMap[key] = //... whatever as SomeType ;
 }
 }
}

Does not work:

interface MyMap {
 myKey1: SomeType;
 myKey2: SomeType;
}
const myObject = new MyClass<MyMap>('myKey1', 'myKey2');
// TS2344: Type MyMap does not satisfy the constraint GenericMap
Index signature for type string is missing in type MyMap

Works but it is not type-safe anymore:

interface MyMap {
 [key: string]: SomeType;
 myKey1: SomeType;
 myKey2: SomeType;
}
const myObject = new MyClass<MyMap>('myKey1', 'myKey2', 'someRandomKey');
// 'someRandomKey' is now allowed but should not

Solution:

type MyMap = {
 myKey1: SomeType;
 myKey2: SomeType;
}
const myObject = new MyClass<MyMap>('myKey1', 'myKey2');
// works and does not allow random keys

Read more about index signatures: https://blog.herodevs.com/typescripts-unsung-hero-index-signatures-ddc3d1e34c9f

answered Mar 26, 2024 at 13:09

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.