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
}
})
13 Answers 13
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.
10 Comments
o.dic['x'] = 1
). Example updated.[key: string]: string | number
but it can cast [key: string]: any
despite an interface only having strings and numbersbar({ ...hash })
works for me.In my case, it was just necessary to use type
instead of interface
.
3 Comments
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'
3 Comments
myVal
typing + autocomplete featuremyInterface
was only meant to have myVal
property, now it can have any property.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
1 Comment
You can solve this problem by doing foo({...o})
playground
1 Comment
Here are my two cents:
type Copy<T> = { [K in keyof T]: T[K] }
genericFunc<SomeType>() // No index signature
genericFunc<Copy<SomeType>>() // No error
Comments
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.
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.
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.
Comments
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.
Comments
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.
Comments
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);
Comments
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 ofinterface 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);
1 Comment
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.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);
Comments
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
type
rather thaninterface
in that case, because interfaces require by design explicit index signatures in those cases.