Managing objects with unknown structures in TypeScript using index signatures

TypeScript

When using TypeScript, we usually strive to define the exact type of data we’re dealing with. Sometimes it is not possible, though. In this article, we look into various options on how to handle an object that has a structure we don’t know. A good example is implementing a JSON editor.

The object type

One of the first things that might come to mind when working with an object we know nothing about is the type.

The data types in JavaScript fall into one of the two categories: primitive values and objects.

1. Primitive values

  • Boolean
  • Null
  • Undefined
  • Number
  • BigInt
  • String
  • Symbol

2. Objects

Every value that is not primitive is considered an object, including arrays, and this is what the type in TypeScript describes. Unfortunately, there is a big chance that it might not fit your use case. The above is an issue big enough that the @typescript-eslint/eslint-plugin package used to throw an error by default when using the type. The plugin maintainers changed it just a few months ago.

Don’t use as a type. The  type is currently hard to use (see this issue).
Consider using instead, as it allows you to more easily inspect and use the keys.

The issue with the type is that it is not straightforward to use.

Unfortunately, the above code causes the following error:

Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index type ‘{}’.
No index signature with a parameter of type ‘string’ was found on type ‘{}’.

The above issue is caused by the operator’s inability to widen the type when used. People have discussed this issue for years, and a PR might fix it. Until then, we should look for other solutions.

The Object type with the uppercase “O”

The type looks very similar, and we need to watch out for it. It describes instances of the class, but its usage is discouraged in the context of dictionaries.

The @typescript-eslint/eslint-plugin package complains when we use the type:

Don’t use as a type. The type actually means “any non-nullish value”, so it is marginally better than .
– If you want a type meaning “any object”, you probably want instead.
– If you want a type meaning “any value”, you probably want instead.

Unfortunately, the error message is correct, and the type accepts primitive values other than null. It might be the case because we can access the properties of via the primitive values.

Because of the above, the following code does not result in an error:

The index signature

The index signature is a fitting way to handle objects with properties we know nothing about. Its syntax mimics a regular property, but instead of writing a standard property name, we define the type of keys and the properties.

Above, we state that a is an object that can have any number of properties of type . Therefore, we can use it with the function we’ve defined at the beginning of this article.

Since we’ve set every property of the to be of type , we’ve had to check if they are strings above before using it.

If we’re confident that all properties of an object are strings, for example, we can reflect that in our type.

It is essential to notice that the type contains all possible properties, which might cause some issues for us.

To deal with the above issue, we can modify the type slightly.

The limitations of the index signature

The index signatures have a few limitations that we should know of. For example, we can only use strings, numbers, and symbols for keys.

TypeScript allows symbols for keys in the index signatures since version 4.4

We can define multiple index signatures, but we need to make sure the types of our properties are compatible with each other.

The above type works fine because we’ve used in both index signatures.

Unfortunately, It is also not straightforward to use enums and string unions.

An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.

We can deal with the above issue by creating multiple index signatures or using the keyword.

The {} type

It is also worth mentioning the type. Unfortunately, it does not mean “any empty object” and accepts any non-null value as well. The @typescript-eslint/eslint-plugin package throws an error when we use it.

Don’t use {} as a type. {} actually means “any non-nullish value”.

If we want to define the type for an empty object for some reason, we can use the type and an index signature.

If you want to know more about the type, check out Understanding any and unknown in TypeScript. Difference between never and void

Empty interfaces behave in the same way as the type, and this is why it is worth using the no-empty-interface ESLint rule.

The Record type

Instead of the index signatures, we can use the utility type. It is generic and accepts two types: the type of the keys and the type of the values.

Above, we define the argument to be an object that contains any number of properties of type .

Let’s look under the hood of the type:

From the above definition, we can see that uses the index signatures under the hood. It might come in handy and make our code a bit more readable. It also makes it simpler to work with enums and unions.

Summary

In this article, we’ve gone through multiple types that we can use when working with objects. We’ve learned about various types such as , , and , that we probably should not use. To deal with our use case, we’ve learned what the index signature is. We’ve also got to know the type and how it can simplify our code in some instances. All of the above can come in handy when dealing with situations that involve dictionaries we know little about.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments