154

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
  • 30,738
  • 21
  • 105
  • 131
Manu Artero
  • 9,238
  • 6
  • 58
  • 73
  • I am looking for answers to this one: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/24469 – Alexander Mills Mar 23 '18 at 00:41
  • 2
    It seems as of now this exact code passes in Typescript without any issues: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgCLAWYB7EcoCeyA3gFDIXIAmGAXCeZUwNp4C2E9AzmFKAOYBdeiACubAEbQANIwoBfUotIxRITDhDIY2bAAps9dBtz4CAShLKANhDDJsyALwMmNBPTJMmAcjg-6AEZZb0ofCQDkACYQykVlHX1scwBuIA – Advait Junnarkar Jan 12 '22 at 01:22
  • 2
    The same issue can still occur in other scenarios like the one described in [this TS issue](https://github.com/microsoft/TypeScript/issues/15300). 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. – Rene Hamburger May 03 '23 at 13:08

11 Answers11

127

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.

Tim Perry
  • 11,766
  • 1
  • 57
  • 85
  • > With the 2nd signature you are. Actually My IDE's doesnot allow me to do `o.dic.x` – Manu Artero May 03 '16 at 13:58
  • 1
    Good catch, sorry, that should've been index syntax (`o.dic['x'] = 1`). Example updated. – Tim Perry May 03 '16 at 14:01
  • 6
    Update! TypeScript 2 should now automatically do this conversion for you, if it's valid: https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#implicit-index-signatures – Tim Perry Jul 12 '16 at 14:50
  • 38
    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 – Ivan Castellanos Feb 11 '19 at 01:09
  • 29
    Spread operator helps in my case - I don't need to modify the object, so `bar({ ...hash })` works for me. – Ivan Yarych Oct 07 '20 at 18:06
  • 14
    As of today, it seems to work, so long as you use types, rather than interfaces. See @Nikolay's answer. – Maciej Krawczyk Apr 13 '21 at 08:24
  • 1
    Is the reason that typescript objects to the `foo(o)` call when `foo` declares `o` as `{ dic: { [name: string]: number } }` is that the interface allows `foo` to add additional properties to `o`, which would break its inferred type of `{ dic: { a: number, b: number } }`? – David Hull Nov 24 '21 at 17:25
  • Type works but Interface does not. See https://codesandbox.io/s/type-vs-interface-kw1j0v?file=/src/index.ts – Frank Fang Nov 24 '22 at 15:46
43

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.

Lewis
  • 4,285
  • 1
  • 23
  • 36
  • 1
    This is the way to go if you don't want to loose `myVal` typing + autocomplete feature – Amaury Liet Mar 11 '21 at 17:00
  • 32
    but you lose type safety when you do that. `myInterface` was only meant to have `myVal` property, now it can have any property. – Maciej Krawczyk Apr 13 '21 at 07:59
  • 1
    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. – Lewis May 03 '21 at 19:06
37

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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Nikolay Podolnyy
  • 931
  • 10
  • 19
  • 4
    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](https://www.typescriptlang.org/play?ts=4.3.5#code/C4TwDgpgBAsiAq5oF4oG8CwAoKuAmAlgMYBc6UA2gHYCGAthGQM7ABOBVA5gLplUCudAEYRWUAL7ZxUbNgBm-KkWAEA9lShzVqgBQ0ycRJACU6SVmwdgouTSLQ4ASSrXWt++my58xMmkq0DMxsHDx8giJi5ubYROosUKoGIM6u7iieON6EpJne+VAA5DSFZADMADReBbiFQqVQAKzVuNGyWFq6qqYA9D1QoJBQoqyqYgAWohDYQA) works as expected. Maybe your answer is outdated (I'm using typescript 4.3.5) – TmTron Aug 26 '21 at 06:49
29

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

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jöcker
  • 5,281
  • 2
  • 38
  • 44
26

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

meblum
  • 1,654
  • 12
  • 23
  • 1
    This is not much more than a link-only answer. Can you [elaborate](https://stackoverflow.com/posts/67219058/edit)? (But ******* ***without*** ******* "Edit:", "Update:", or similar - the answer should appear as if it was written today.) – Peter Mortensen Sep 27 '22 at 21:41
19

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
  • 30,738
  • 21
  • 105
  • 131
Tim Baas
  • 6,035
  • 5
  • 45
  • 72
10

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

P.S.

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

WebBrother
  • 1,447
  • 20
  • 31
4

This error is legitimate. You should instead write the code below to mark types as immutable:

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

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

const o = Object.freeze({
    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.a;

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

Alexandre Annic
  • 9,942
  • 5
  • 36
  • 50
3

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.

alex
  • 76
  • 2
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
  • 30,738
  • 21
  • 105
  • 131
aWebDeveloper
  • 36,687
  • 39
  • 170
  • 242
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);
Alex Shul
  • 500
  • 7
  • 22