How do I prevent the error "Index signature of object type implicitly has an 'any' type" when compiling typescript with noImplicitAny flag enabled?
Asked Answered
C

16

384

I always compile TypeScript with the flag --noImplicitAny. This makes sense as I want my type checking to be as tight as possible.

My problem is that with the following code I get the error:

Index signature of object type implicitly has an 'any' type
interface ISomeObject {
    firstKey:   string;
    secondKey:  string;
    thirdKey:   string;
}

let someObject: ISomeObject = {
    firstKey:   'firstValue',
    secondKey:  'secondValue',
    thirdKey:   'thirdValue'
};

let key: string = 'secondKey';

let secondValue: string = someObject[key];

Important to note is that the idea is that the key variable comes from somewhere else in the application and can be any of the keys in the object.

I've tried explicitly casting the type by:

let secondValue: string = <string>someObject[key];

Or is my scenario just not possible with --noImplicitAny?

Chromaticness answered 6/10, 2015 at 11:2 Comment(0)
S
404

Adding an index signature will let TypeScript know what the type should be.

In your case that would be [key: string]: string;

interface ISomeObject {
    firstKey:      string;
    secondKey:     string;
    thirdKey:      string;
    [key: string]: string;
}

However, this also enforces all of the property types to match the index signature. Since all of the properties are a string it works.

While index signatures are a powerful way to describe the array and 'dictionary' pattern, they also enforce that all properties match their return type.

Edit:

If the types don't match, a union type can be used [key: string]: string|IOtherObject;

With union types, it's better if you let TypeScript infer the type instead of defining it.

// Type of `secondValue` is `string|IOtherObject`
let secondValue = someObject[key];
// Type of `foo` is `string`
let foo = secondValue + '';

Although that can get a little messy if you have a lot of different types in the index signatures. The alternative to that is to use any in the signature. [key: string]: any; Then you would need to cast the types like you did above.

Seamstress answered 6/10, 2015 at 11:37 Comment(6)
And if your interface looks like interface ISomeObject { firstKey: string; secondKey: IOtherObject; } this isn't possible I guess?Chromaticness
Thanks! The combination of an any type together with casting a type per case seems a sold way to go.Chromaticness
Hi, How to handle the "anyObject[key: Object]['name']"?Pasco
or say something like _obj = {}; let _dbKey = _props[key]['name']; _obj[_dbKey] = this[key]; here _props is object and object[key] also will return an object which will have the name property.Pasco
So the solution to narrow a key is to change the interface of the object!? Is this not the world upside down...?Containment
the "answer" works I guess but it doesn't tells you the root cause of the issue I totally there is way betters to do itJoline
U
192

Another way to avoid the error is to use the cast like this:

let secondValue: string = (<any>someObject)[key]; (Note the parenthesis)

The only problem is that this isn't type-safe anymore, as you are casting to any. But you can always cast back to the correct type.

ps: I'm using typescript 1.7, not sure about previous versions.

Uncial answered 4/2, 2016 at 18:39 Comment(2)
To avoid tslint warnings, you may also use: let secondValue: string = (someObject as any)[key];Mcclung
@Mcclung this was useful :)Vivisection
T
115

TypeScript 2.1 introduced elegant way to handle this issue.

const key: (keyof ISomeObject) = 'secondKey';
const secondValue: string = someObject[key];

We can access all object property names during compilation phase by keyof keyword (see changelog).

You only need to replace string variable type with keyof ISomeObject. Now compiler knows key variable is allowed to contain only property names from ISomeObject.

Full example:

interface ISomeObject {
    firstKey:   string;
    secondKey:  string;
    thirdKey:   number;
}

const someObject: ISomeObject = {
    firstKey:   'firstValue',
    secondKey:  'secondValue',
    thirdKey:   3
};

const key: (keyof ISomeObject) = 'secondKey';
const secondValue: string = someObject[key];

// You can mix types in interface, keyof will know which types you refer to.
const keyNumber: (keyof ISomeObject) = 'thirdKey';
const numberValue: number = someObject[keyNumber];

Live code on typescriptlang.org (set noImplicitAny option)

Further reading with more keyof usages.

Tiller answered 23/11, 2017 at 19:0 Comment(1)
However it will not work if we declare key as const key = (keyof ISomeObject) = 'second' + 'Key'Therapsid
K
72

The following tsconfig setting will allow you to ignore these errors - set it to true.

suppressImplicitAnyIndexErrors

Suppress noImplicitAny errors for indexing objects lacking index signatures.

Khalid answered 9/8, 2016 at 4:34 Comment(5)
that is something You shouldn't do - probably someone in Your team has explicitly set this compiler option to make the code more bullet-proof!Snare
I disagree this is exactly what this option was made for : Allow brackets notation with --noImplicitAny. Match perfectly op's question.Maxama
I agree with @Maxama . This is also the only option if modifying the interface is not possible. For example, with internal interfaces like XMLHttpRequest.Papke
I also agree with @Ghetolay. I'm curious how this is qualitatively different from Pedro Villa Verde's answer (apart from the fact that the code is less ugly). We all know that accessing an object property using a string should be avoided if possible, but we sometimes enjoy that liberty while understanding the risks.Dentate
It's just tradeoffs. Pick what you like: less error surface area and strict index access, or have more surface area for errors and easily access unknown indices. The TS2.1 keyof operator can help keep everything strict, see Piotr's answer!Singleaction
C
45

use keyof typeof

const cat = {
    name: 'tuntun'
}

const key: string = 'name' 

cat[key as keyof typeof cat]
Conjoint answered 27/2, 2019 at 9:21 Comment(5)
works like charm. please explain the use of keyof typeofAerostatics
@Aerostatics use as keyof when you want to say that the key you're referring to belongs to an interface. Use as keyof typeof when you don't have an interface for some object and want the compiler to guess the type of object you're referring to.Altis
Don't do this actually. This is just as wrong as using key: any. Instead you should make the type of key actually BE keyof typeof cat: const key: keyof typeof cat = 'name' That way you're not blindly casting strings to make the compiler quiet. Or stop calling key a string, make its type inferred: const key = 'name'. Or use as const to guarantee the type is not string, in case you want key to be variable: let key = 'name' as const. SO many better options than just lying to the compiler so it stays quiet.Wooldridge
@Wooldridge key maybe come from other places, such as user input or a parsed json.Conjoint
@Conjoint Your code gives the false impression that there is any type safety whatsoever, and there isn't, you effectively turned off type checking. It's a more confusing way of just saying "as any" (i.e. turn off type checking) because it's just saying "take whatever key is and pretend it's a key in cat. In the example, the input is NOT user input or JSON, it's a very common use case and we should be showing people how to do it right and professional instead of hacking around type safety.Wooldridge
T
32

The 'keyof' solution mentioned above works. But if the variable is used only once e.g looping through an object etc, you can also typecast it.

for (const key in someObject) {
    sampleObject[key] = someObject[key as keyof ISomeObject];
}
Tinkle answered 7/5, 2018 at 4:7 Comment(1)
Thanks. This works for arbitrary key access when iterating another object's keys.Succussion
A
8

Declare the object like this.

export interface Thread {
    id:number;
    messageIds: number[];
    participants: {
        [key:number]: number
    };
}
Aguiar answered 5/10, 2017 at 16:34 Comment(0)
K
8

Create an interface to define the 'indexer' interface

Then create your object with that index.

Note: this will still have same issues other answers have described with respect to enforcing the type of each item - but that's often exactly what you want.

You can make the generic type parameter whatever you need : ObjectIndexer< Dog | Cat>

// this should be global somewhere, or you may already be 
// using a library that provides such a type
export interface ObjectIndexer<T> {
  [id: string]: T;
}

interface ISomeObject extends ObjectIndexer<string>
{
    firstKey:   string;
    secondKey:  string;
    thirdKey:   string;
}

let someObject: ISomeObject = {
    firstKey:   'firstValue',
    secondKey:  'secondValue',
    thirdKey:   'thirdValue'
};

let key: string = 'secondKey';

let secondValue: string = someObject[key];

Typescript Playground


You can even use this in a generic constraint when defining a generic type:

export class SmartFormGroup<T extends IndexableObject<any>> extends FormGroup

Then T inside the class can be indexed :-)

Kenelm answered 23/1, 2019 at 2:11 Comment(2)
I don't think there's a standard 'built-in' interface for Dictionary that represents { [key: string]: T }, but if there ever is please edit this question to remove my ObjectIndexer.Kenelm
Simple and very underrated answer.Carangid
K
8

No indexer? Then make your own!

I've globally defined this as an easy way to define an object signature. T can be any if needed:

type Indexer<T> = { [ key: string ]: T };

I just add indexer as a class member.

indexer = this as unknown as Indexer<Fruit>;

So I end up with this:

constructor(private breakpointResponsiveService: FeatureBoxBreakpointResponsiveService) {

}

apple: Fruit<string>;
pear: Fruit<string>;

// just a reference to 'this' at runtime
indexer = this as unknown as Indexer<Fruit>;

something() {

    this.indexer['apple'] = ...    // typed as Fruit

Benefit of doing this is that you get the proper type back - many solutions that use <any> will lose the typing for you. Remember this doesn't perform any runtime verification. You'll still need to check if something exists if you don't know for sure it exists.

If you want to be overly cautious, and you're using strict you can do this to reveal all the places you may need to do an explicit undefined check:

type OptionalIndexed<T> = { [ key: string ]: T | undefined };

I don't usually find this necessary since if I have as a string property from somewhere I usually know that it's valid.

I've found this method especially useful if I have a lot of code that needs to access the indexer, and the typing can be changed in just one place.

Note: I'm using strict mode, and the unknown is definitely necessary.

The compiled code will just be indexer = this, so it's very similar to when typescript creates _this = this for you.

Kenelm answered 31/1, 2019 at 1:4 Comment(1)
Some cases you may be able to use Record<T> type instead - I'm not able right now to research the fine details of this but for some limited cases it may work better.Kenelm
G
7

Similar to @Piotr Lewandowski's answer, but within a forEach:

const config: MyConfig = { ... };

Object.keys(config)
  .forEach((key: keyof MyConfig) => {
    if (config[key]) {
      // ...
    }
  });
Gird answered 30/11, 2017 at 22:1 Comment(1)
How did you get this to work? I'm trying the same thing (ts 3.8.3), although it throws an error saying: Argument of type '(field: "id" | "url" | "name") => void' is not assignable to parameter of type '(value: string, index: number, array: string[]) => void'. My code looks like so Object.keys(components).forEach((comp: Component) => {...}, where Component is a type (like MyConfig).Machicolation
Q
7

Declare type which its key is string and value can be any then declare the object with this type and the lint won't show up

type MyType = {[key: string]: any};

So your code will be

type ISomeType = {[key: string]: any};

    let someObject: ISomeType = {
        firstKey:   'firstValue',
        secondKey:  'secondValue',
        thirdKey:   'thirdValue'
    };

    let key: string = 'secondKey';

    let secondValue: string = someObject[key];
Quaternion answered 11/10, 2019 at 19:15 Comment(0)
A
3

The simplest solution that I could find using Typescript 3.1 in 3 steps is:

1) Make interface

interface IOriginal {
    original: { [key: string]: any }
}

2) Make a typed copy

let copy: IOriginal = (original as any)[key];

3) Use anywhere (JSX included)

<input customProp={copy} />
Aphasia answered 3/12, 2018 at 6:47 Comment(0)
A
2

At today better solution is to declare types. Like

enum SomeObjectKeys {
    firstKey = 'firstKey',
    secondKey = 'secondKey',
    thirdKey = 'thirdKey',
}

let someObject: Record<SomeObjectKeys, string> = {
    firstKey:   'firstValue',
    secondKey:  'secondValue',
    thirdKey:   'thirdValue',
};

let key: SomeObjectKeys = 'secondKey';

let secondValue: string = someObject[key];
Acrostic answered 11/9, 2018 at 13:2 Comment(0)
C
1

There is no need to use an ObjectIndexer<T>, or change the interface of the original object (like suggested in most of the other answers). You can simply narrow the options for key to the ones that are of type string using the following:

type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];

This great solution comes from an answer to a related question here.

Like that you narrow to keys inside T that hold V values. So in your case to to limit to string you would do:

type KeysMatching<ISomeObject, string>;

In your example:

interface ISomeObject {
    firstKey:   string;
    secondKey:  string;
    thirdKey:   string;
}

let someObject: ISomeObject = {
    firstKey:   'firstValue',
    secondKey:  'secondValue',
    thirdKey:   'thirdValue'
};

let key: KeysMatching<SomeObject, string> = 'secondKey';

// secondValue narrowed to string    
let secondValue = someObject[key];

The advantage is that your ISomeObject could now even hold mixed types, and you can anyway narrow the key to string values only, keys of other value types will be considered invalid. To illustrate:

interface ISomeObject {
    firstKey:   string;
    secondKey:  string;
    thirdKey:   string;
    fourthKey:  boolean;
}

let someObject: ISomeObject = {
    firstKey:   'firstValue',
    secondKey:  'secondValue',
    thirdKey:   'thirdValue'
    fourthKey:   true
};


// Type '"fourthKey"' is not assignable to type 'KeysMatching<ISomeObject, string>'.(2322)
let otherKey: KeysMatching<SomeOtherObject, string> = 'fourthKey';

let fourthValue = someOtherObject[otherKey];

You find this example in this playground.

Containment answered 21/10, 2020 at 6:25 Comment(0)
S
0

I had two interfaces. First was child of other. I did following:

  1. Added index signature in parent interface.
  2. Used appropriate type using as keyword.

Complete code is as below:

Child Interface:

interface UVAmount {
  amount: number;
  price: number;
  quantity: number;
};

Parent Interface:

interface UVItem  {
// This is index signature which compiler is complaining about.
// Here we are mentioning key will string and value will any of the types mentioned.
  [key: string]:  UVAmount | string | number | object;

  name: string;
  initial: UVAmount;
  rating: number;
  others: object;
};

React Component:

let valueType = 'initial';

function getTotal(item: UVItem) {
// as keyword is the dealbreaker.
// If you don't use it, it will take string type by default and show errors.
  let itemValue = item[valueType] as UVAmount;

  return itemValue.price * itemValue.quantity;
}

Siple answered 13/8, 2020 at 7:54 Comment(0)
J
-1

cause

You can only use types when indexing, meaning you can’t use a const to make a variable reference:

example

type Person = { age: number; name: string; alive: boolean };

const key = "age";
type Age = Person[key];

result

Type 'any' cannot be used as an index type.

Solution

use types to refer props

example

type key = "age";
type Age = Person[key];

result

type Age = number
Joline answered 3/9, 2021 at 16:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.