TypeScript: Index signature is missing in type
Asked Answered
L

12

169

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
  }
})
Leeke answered 3/5, 2016 at 13:45 Comment(3)
I am looking for answers to this one: github.com/DefinitelyTyped/DefinitelyTyped/issues/24469Falconiform
It seems as of now this exact code passes in Typescript without any issues: typescriptlang.org/play?#code/…Carbylamine
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.Paleobiology
L
135

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.

Legit answered 3/5, 2016 at 13:52 Comment(8)
> With the 2nd signature you are. Actually My IDE's doesnot allow me to do o.dic.xLeeke
Good catch, sorry, that should've been index syntax (o.dic['x'] = 1). Example updated.Legit
Update! TypeScript 2 should now automatically do this conversion for you, if it's valid: github.com/Microsoft/TypeScript/wiki/…Legit
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 numbersGustafson
Spread operator helps in my case - I don't need to modify the object, so bar({ ...hash }) works for me.Pirzada
As of today, it seems to work, so long as you use types, rather than interfaces. See @Nikolay's answer.Ichthyoid
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 } }?Kerb
Type works but Interface does not. See codesandbox.io/s/type-vs-interface-kw1j0v?file=/src/index.tsReseat
O
48

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.

Olga answered 28/12, 2020 at 5:7 Comment(3)
This is the way to go if you don't want to loose myVal typing + autocomplete featureAlongshore
but you lose type safety when you do that. myInterface was only meant to have myVal property, now it can have any property.Ichthyoid
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.Olga
L
43

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
Laodicean answered 2/1, 2021 at 17:16 Comment(1)
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)Bosanquet
B
43

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

Berate answered 8/3, 2022 at 11:29 Comment(2)
Same for me, but why is this?Demmer
See github.com/microsoft/TypeScript/issues/15300. By design interfaces and types behave differently when it comes to index signatures.Paleobiology
P
31

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

Pesce answered 22/4, 2021 at 18:40 Comment(1)
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.)Laveralavergne
F
21

Here are my two cents:

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

genericFunc<SomeType>() // No index signature

genericFunc<Copy<SomeType>>() // No error
Faline answered 20/8, 2021 at 18:56 Comment(0)
T
13

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.

Tindall answered 16/11, 2022 at 21:34 Comment(0)
A
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.

Aton answered 23/5, 2021 at 12:47 Comment(0)
B
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.

Balbinder answered 30/9, 2022 at 19:38 Comment(0)
O
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);
Outgroup answered 11/8, 2022 at 5:17 Comment(0)
R
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);
Rembert answered 30/8, 2023 at 13:32 Comment(0)
L
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

Lille answered 26/3 at 13:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.