which is the best way to define an object in typescript and access it values?

Viewed 892

I have a Object of format

export const IconSizeMap = {
  'extra-small': '0.75rem',
  small: '1rem',
  medium: '1.5rem',
  large: '2rem',
  'extra-large': '4rem'
};

Being new to typescript i could not understand what the error is

It gives an error

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ 'extra-small': string; small: string; medium: string; large: string; 'extra-large': string; }'.
  No index signature with a parameter of type 'string' was found on type '{ 'extra-small': string; small: string; medium: string; large: string; 'extra-large': string; }'.ts(7053)

when i try to use

IconSizeMap[size]

How do i solve this error ?

3 Answers

IconSizeMap[size] is trying to use a string as an index on an object that isn't defined with string indexing, which is why you're getting that error.

Your options are:

  1. Add a string index signature to the object, or

  2. Don't use an general string to index into the object

Which you do is up to your use case. An index signature means TypeScript can't proactively check for you that size matches a property on the object. But using only specific property names means listing those property names in more than one place if your starting point is a string that could have any value.

Here's an example of #1:

export const IconSizeMap: {[key: string]: string} = {
// −−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^
  'extra-small': '0.75rem',
  small: '1rem',
  medium: '1.5rem',
  large: '2rem',
  'extra-large': '4rem'
};

Playground link

Here's an example of #2:

function getIconSize(size: string) {
    switch (size) {
        case 'extra-small':
            return IconSizeMap['extra-small'];
        case 'small':
            return IconSizeMap.small;
        case 'medium':
            return IconSizeMap.medium;
        case 'large':
            return IconSizeMap.large;
        case 'extra-large':
            return IconSizeMap['extra-large'];
        default:
            throw new Error(`Invalid 'size' for IconSizeMap: ${size}`);
    }
}

Notice how we use specific string literals, not just any string, when accessing the object.

Playground link

You're getting this error because your tsconfig.json has the "noImplicitAny": true enabled. It means that TypeScript will look for and inform you of places where you use a value whose type is not known, and where you did not explicitly tell him which type it should be.

Your solutions to get rid of it are either to :

  • Disable noImplicitAny in the configuration (But I guess there's a reason it's here so I wouldn't recommend that)
  • Tell TypeScript the type of what you're using that causes the error.

Here, TypeScript recognized each of the 5 keys you defined (extra-small, small, medium, large and extra-large) because you assigned them a value that is a string, so it can tell they're strings. However, only those 5 strings are keys.

When you're using a "generic" string to access your object such as what I assume your size is, you're not sure whether it corresponds to one of the 5 keys. So, TypeScript warns you of that.

One solution to that could be to tell the compiler you're sure about what you're doing, and specifying the type of size more precisely. For example :

export const IconSizeMap = {
  'extra-small': '0.75rem',
  small: '1rem',
  medium: '1.5rem',
  large: '2rem',
  'extra-large': '4rem'
};

let size: keyof typeof IconSizeMap; // Means "I'm sure that size is one of those strings
size = "small";

console.log(IconSizeMap[size]);

Playground link

Since T.J. Crowder's answer is correct, I'll just add some comments in case it helps understand better what is going on.

You have to remember that TypeScript adds static typing to JavaScript. That means that every entity defined in the language has a definite type, whether defined by you or inferred implicitly by TypeScript, and TypeScript will enforce observance of that type.

In your case, TypeScript implicitly infers that your IconSizeMap object is of a definite type. It's not a generic JavaScript object which can have any string key, but an object with some definite keys: extra-small, small, etc. TypeScript infers that from your definition of the object.

Once TypeScript has inferred a type for an entity, it will enforce it for every use you make of that entity. That means that if you try something like IconSizeMap['huge'] it will complain, since 'huge' was not registerd as a valid key in the type inferred by TypeScript.

This is quite easy to understand since 'huge' is explictly not contained in the object. But it has trickier consequences, like the problem you mentioned. If you are using variable size of type string to access the object properties, TypeScript will also complain because it can be possible that the value of size be a string not registered as one of the object keys. That's the reason for T.J.'s solutions:

  1. You either explicitly define the object's type as allowing any string key.
  2. You restrict the type of your string variables that will refer to object keys, so that they cannot be any string but just one of the strings contained in your object.

I suppose the second solution is what you need, since your object seems to be a fixed map, that doesn't allow further string keys. You can let TypeScript infer that implicitly, as in T.J's solution that uses a switch with only the expected string literals.

However, in situations like this I sometimes explicitly define union types containing those string literals. For example:

type IconSize = 'extra-small' | 'small' | 'medium' | 'large ' | 'extra-large';

I can then define variables explicitly with that type, and TypeScript will help me avoid assigning different string values to that variable:

let size: IconSize;

This means that TypeScript will rule out any attempt at setting a different string value to size. Since size will have one of the expected values, the next time you try IconSizeMap[size] TypeScript will not complain.

Related