maaartinus
maaartinus

Reputation: 46422

How to declare a typed object with arbitrary string keys - Index signature is missing?

I need to declare an object having arbitrary string keys and values of type number or string, i.e., something like in this question.

So I wrote

interface AnyObjectWithNumberAndStringValues {
  [s: string]: number | string;
}

and a test function

function f(x: AnyObjectWithNumberAndStringValues) {
  console.log(x);
}

and it sort of work. I can call

f({ id: 123, name: "noname" })

But now, I have an interface and an object like

interface ISupplier {
  id: number;
  name: string;
}

const o: ISupplier = { id: 123, name: "noname" };

and calling f(o) produces the following error:

Argument of type 'ISupplier' is not assignable to parameter of type 'AnyObjectWithNumberAndStringValues'. Index signature is missing in type 'ISupplier'.ts(2345)

More precisely: How can I declare that f accepts any object as long as its values are only numbers and strings?

See this sandbox for a full example. Note that it seems to be non-deterministic: The error may or may not appear!

Upvotes: 2

Views: 630

Answers (1)

jcalz
jcalz

Reputation: 328272

It is definitely confusing to figure out what sorts of types are assignable to and from an indexable type. The reason why

f({ id: 123, name: "noname" }); // okay

works is because the argument is an object literal, which is given an implicit index signature. At the time this occurs, the compiler knows that the parameter is an object literal and therefore it doesn't have any unknown properties on it. Thus it is safe to treat it as something assignable to AnyObjectWithNumberAndStringValues.

The reason why

const o: ISupplier = { id: 123, name: "noname" };
f(o); // error

doesn't work is because o is given a type annotation of ISupplier. By the time f(o) is called, the compiler has forgotten about the specific properties inside o. For all the compiler knows there may be unknown non-string and non-number properties in there.

Consider the following code:

interface ISupriser extends ISupplier {
  surprise: boolean;
}
const s: ISupriser = { id: 123, name: "noname", surprise: true };
const uhOh: ISupplier = s;
f(uhOh); // error makes sense now

An ISurpriser contains a boolean property, so you wouldn't want to call f() with it. But every ISupriser is also an ISupplier. The variable uhOh is just as valid of an ISupplier as your o was. And all the compiler remembers about both o and uhOh is that they are instances of ISupplier. That's why f(o) fails... it's to prevent you from accidentally calling f(uhOh).


Note that any workaround you do which allows you to call f() or a similar function on something that isn't an object literal has the potential to allow unexpected arguments, with bad properties the compiler doesn't know about (or doesn't remember). But let's look at these workarounds anyway:

It has been noted that one workaround is to use a type alias for ISupplier instead of an interface. Apparently the trade-off here is that it's not as easy to extend type aliases (you can get the same effect with intersections) so it's "safe enough" to consider it as having an implicit index signature:

type TSupplier = {
  id: number;
  name: string;
};
const p: TSupplier = { id: 123, name: "noname" };
f(p); // okay

It might not be "as easy" to add undesirable properties as it is with an interface, but it's still easy enough:

const ohNo: TSupplier = uhOh; // okay
f(ohNo); // no error, but there probably should be one!

Another workaround, especially if you can't change ISupplier, is to use a constrained generic type in your function instead of an index signature, like this:

function g<T extends Record<keyof T, number | string>>(x: T) {
  console.log(x);
}

The g() function basically only accepts arguments whose known properties are assignable to number | string:

g({ id: 123, name: "noname" }); // okay
g(o); // okay
g(s); // error, "surprise" is incompatible

Again, though, if the compiler doesn't know about a property, it can't check it, so these also have no error:

g(uhOh); // no error, but there probably should be one
g(ohNo); // no error, but there probably should be one

Anyway, hope that helps you. Good luck!

Link to code

Upvotes: 1

Related Questions