693

Trying out TypeScript for a React project and I'm stuck on this error:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ train_1: boolean; train_2: boolean; train_3: boolean; train_4: boolean; }'.
  No index signature with a parameter of type 'string' was found on type '{ train_1: boolean; train_2: boolean; train_3: boolean; train_4: boolean; }'

Which appears when I try to filter the array in my component

.filter(({ name }) => plotOptions[name]);

So far I looked at the article "Indexing objects in TypeScript" (https://dev.to/kingdaro/indexing-objects-in-typescript-1cgi) since it had a similar error, but I tried to add the index signature to type plotTypes and I still get the same error.

My component code:

import React, { Component } from "react";
import createPlotlyComponent from "react-plotly.js/factory";
import Plotly from "plotly.js-basic-dist";
const Plot = createPlotlyComponent(Plotly);

interface IProps {
  data: any;
}

interface IState {
  [key: string]: plotTypes;
  plotOptions: plotTypes;
}

type plotTypes = {
  [key: string]: boolean;
  train_1: boolean;
  train_2: boolean;
  train_3: boolean;
  train_4: boolean;
};

interface trainInfo {
  name: string;
  x: Array<number>;
  y: Array<number>;
  type: string;
  mode: string;
}

class FiltrationPlots extends Component<IProps, IState> {
  readonly state = {
    plotOptions: {
      train_1: true,
      train_2: true,
      train_3: true,
      train_4: true
    }
  };
  render() {
    const { data } = this.props;
    const { plotOptions } = this.state;

    if (data.filtrationData) {
      const plotData: Array<trainInfo> = [
        {
          name: "train_1",
          x: data.filtrationData.map((i: any) => i["1-CumVol"]),
          y: data.filtrationData.map((i: any) => i["1-PressureA"]),
          type: "scatter",
          mode: "lines"
        },
        {
          name: "train_2",
          x: data.filtrationData.map((i: any) => i["2-CumVol"]),
          y: data.filtrationData.map((i: any) => i["2-PressureA"]),
          type: "scatter",
          mode: "lines"
        },
        {
          name: "train_3",
          x: data.filtrationData.map((i: any) => i["3-CumVol"]),
          y: data.filtrationData.map((i: any) => i["3-PressureA"]),
          type: "scatter",
          mode: "lines"
        },
        {
          name: "train_4",
          x: data.filtrationData.map((i: any) => i["4-CumVol"]),
          y: data.filtrationData.map((i: any) => i["4-PressureA"]),
          type: "scatter",
          mode: "lines"
        }
      ].filter(({ name }) => plotOptions[name]);
      return (
        <Plot
          data={plotData}
          layout={{ width: 1000, height: 1000, title: "A Fancy Plot" }}
        />
      );
    } else {
      return <h1>No Data Loaded</h1>;
    }
  }
}

export default FiltrationPlots;

Liam
  • 27,717
  • 28
  • 128
  • 190
DLee
  • 7,856
  • 4
  • 19
  • 23

20 Answers20

888

For anyone who stumbles upon this in the future:

If you're getting the TypeScript error

'...expression of type string cannot be used to index...'

then simply specify that the 'expression of type string' is a key of the type of that object. For example,

const someObj:ObjectType = data;
const field = 'username';

// This gives an error
const temp = someObj[field];

// Solution 1: When the type of the object is known
const temp = someObj[field as keyof ObjectType]

// Solution 2: When the type of the object is not known
const temp = someObj[field as keyof typeof someObj]
Vikram Deshmukh
  • 12,304
  • 4
  • 36
  • 38
  • 39
    this show "Type 'any' is not assignable to type 'never" error – Franklin'j Gil'z Dec 11 '21 at 10:34
  • @Franklin'jGil'z Can you share a snippet? If you have a type specified implicitly or explicitly, you shouldn't get that error. – Vikram Deshmukh Dec 12 '21 at 00:14
  • 1
    @VikramDeshmukh thanks for the edit, but I deleted my comment. There was a large amount of edge cases I wasn't factoring in. See this huge discussion about it: https://github.com/microsoft/TypeScript/issues/21732 As far as I can digest, its not yet a solved problem – Chris Jul 05 '22 at 15:11
  • 2
    If you index an object T with a key that is not in this object, the resulting value is undefined. The type you want is T[K] | undefined, not just T[K]. – Romain Vincent Jul 13 '22 at 11:59
  • 9
    Or like `const myObject: Record = {}` – SeanMC Sep 15 '22 at 21:36
  • 6
    Is there a less verbose solution? because this really hurt readability – TOPKAT Dec 19 '22 at 19:01
  • Is there a specific TS setting specific to this particular aspect of the "no implicit any" setting? Because throwing a compiler error in JavaScript from indexing using a string is IMO going too far (though I understand many think this is ok). – wraiford Mar 28 '23 at 14:38
  • Thanks for pointing out this workaround! I just wish there was a way to TS to infer the types of the keys of an interface with builtin Object/Array methods. Considering an object with static keys defined in its corresponding interface, it seems like TS should be able to infer them no problem, but I suppose not today. – Steve Aug 09 '23 at 17:38
377

This happens because you try to access plotOptions property using string name. TypeScript understands that name may have any value, not only property name from plotOptions. So TypeScript requires to add index signature to plotOptions, so it knows that you can use any property name in plotOptions. But I suggest to change type of name, so it can only be one of plotOptions properties.

interface trainInfo {
    name: keyof typeof plotOptions;
    x: Array<number>;
    y: Array<number>;
    type: string;
    mode: string;
}

Now you'll be able to use only property names that exist in plotOptions.

You also have to slightly change your code.

First assign array to some temp variable, so TS knows array type:

const plotDataTemp: Array<trainInfo> = [
    {
      name: "train_1",
      x: data.filtrationData.map((i: any) => i["1-CumVol"]),
      y: data.filtrationData.map((i: any) => i["1-PressureA"]),
      type: "scatter",
      mode: "lines"
    },
    // ...
}

Then filter:

const plotData = plotDataTemp.filter(({ name }) => plotOptions[name]);

If you're getting data from API and have no way to type check props at compile time the only way is to add index signature to your plotOptions:

type tplotOptions = {
    [key: string]: boolean
}

const plotOptions: tplotOptions = {
    train_1: true,
    train_2: true,
    train_3: true,
    train_4: true
}
montrealist
  • 5,593
  • 12
  • 46
  • 68
Fyodor Yemelyanenko
  • 11,264
  • 1
  • 30
  • 38
132

When using Object.keys, the following works:

Object.keys(this)
    .forEach(key => {
      console.log(this[key as keyof MyClass]);
    });
Noel Yap
  • 18,822
  • 21
  • 92
  • 144
  • 12
    Excellent! Why does this solve the issue? – Andru Jan 29 '21 at 08:31
  • @Andru this solves the issue because TS recognizes that the `key` is something that `this` actually has. In fact, we can even get rid of the class name and use `this` instead like `this[key as keyof this]`! – Christos Lytras Mar 27 '21 at 01:41
  • 1
    @ChristosLytras I see. A pity TypeScript has to be told this by a type cast. ..What else could `key` actually be when it's a loop over the elements of `Object.keys`? - In a future TypeScript version such a type cast might not be necessary, I guess. – Andru Apr 10 '21 at 16:49
98

I use this:

interface IObjectKeys {
  [key: string]: string | number;
}

interface IDevice extends IObjectKeys {
  id: number;
  room_id: number;
  name: string;
  type: string;
  description: string;
}

NOTE: "[key: string]" what is it? An object in JavaScript is primarily just a collection of properties made up of key-value pairs. Moreover, the key can only be a string (even for array elements), but the value can be any data type.

If you use the optional property in your object:

interface IDevice extends IObjectKeys {
  id: number;
  room_id?: number;
  name?: string;
  type?: string;
  description?: string;
}

... you should add 'undefined' value into the IObjectKeys interface:

interface IObjectKeys {
  [key: string]: string | number | undefined;
}
Gennady Magomaev
  • 1,157
  • 6
  • 8
  • If i want to make some of the property optional? for example `room_id?:number;` – Oliver D Jan 30 '21 at 00:09
  • You can do this. I think it has nothing to do with this issue. – Gennady Magomaev Feb 02 '21 at 08:00
  • 1
    Nope `?optional` that means maybe `undefined` so should I add it like this `[key: string]: string | number | undefined`. that solves the issue `Property 'room_id' of type 'number | undefined' is not assignable to string index type 'string | number'.` :) – Oliver D Feb 02 '21 at 08:56
  • Oh, I see :). You are right! :) Thank you! – Gennady Magomaev Feb 09 '21 at 16:43
  • 1
    @GennadyMagomaev Hello, I had the same problem and your answer helped me out a lot! I dont fully understand what this means though : "[key: string]: string | number". Does it mean that the key of the object can be a string and after the " :" does it mean it can also be a string or number or ? I got confused... Could you be kind enough to explain it to me please ? – Why u do dis Mar 07 '21 at 16:49
  • Hello, thank you for your question! I added a new description. Look, an object in JavaScript is primarily just a collection of properties made up of key-value pairs. Moreover, the key can only be a string (even for array elements), but the value can be any data type. I had answered your question? – Gennady Magomaev Mar 11 '21 at 13:26
  • 3
    Best, cleanest, and most flexible solution. – AMS777 Jun 02 '21 at 09:20
  • 1
    You can also just extend Record, Replace any with options if you want. –  Nov 05 '21 at 14:55
  • 1
    Thanks, in my case, only add an index in my type [key: string] was enough. – Theo Oliveira Apr 10 '23 at 14:48
82
// bad
const _getKeyValue = (key: string) => (obj: object) => obj[key];

// better
const _getKeyValue_ = (key: string) => (obj: Record<string, any>) => obj[key];

// best
const getKeyValue = <T extends object, U extends keyof T>(key: U) => (obj: T) =>
  obj[key];

Bad - the reason for the error is the object type is just an empty object by default. Therefore it isn't possible to use a string type to index {}.

Better - the reason the error disappears is because now we are telling the compiler the obj argument will be a collection of string/value (string/any) pairs. However, we are using the any type, so we can do better.

Best - T extends empty object. U extends the keys of T. Therefore U will always exist on T, therefore it can be used as a look up value.

Here is a full example:

I have switched the order of the generics (U extends keyof T now comes before T extends object) to highlight that order of generics is not important and you should select an order that makes the most sense for your function.

const getKeyValue = <U extends keyof T, T extends object>(key: U) => (obj: T) =>
  obj[key];

interface User {
  name: string;
  age: number;
}

const user: User = {
  name: "John Smith",
  age: 20
};

const getUserName = getKeyValue<keyof User, User>("name")(user);

// => 'John Smith'

Alternative Syntax

const getKeyValue = <T, K extends keyof T>(obj: T, key: K): T[K] => obj[key];
Alex Mckay
  • 3,463
  • 16
  • 27
  • 4
    I wrote a tiny [npm package](https://github.com/alexandermckay/get-key-value) with this function to make this task easier for those that are new to Typescript. – Alex Mckay Jun 30 '20 at 00:58
  • The package is approximately 38 bytes once packaged and minified. – Alex Mckay Jun 30 '20 at 01:14
59

I fixed this issue by using keyof

messageMap = {
 "Hi": "Hello",
 "Who are you": "My name is Test Sat Bot",
 "What is your role": "Just guide for the user",
}

this ❌

let answer = this.messageMap[question];

replace with ✔️

let answer = this.messageMap[question as keyof typeof this.messageMap];

Here question is type of string

getBotMessage(question: string){    
  let answer = this.messageMap[question as keyof typeof this.messageMap];
}
R15
  • 13,982
  • 14
  • 97
  • 173
  • another reason why TS solves less problems than it creates. You go from 39 chars of valid JS code that works 100% of the time to 71 chars of TS that just compiled down to the original 39 chars. so dumb. – LMS5400 Aug 30 '23 at 20:33
28

I have made a simulation of the problem. looks like the issue is how we should Access Object Properties Dynamically Using Bracket Notation in Typescript

interface IUserProps {
  name: string;
  age: number;
}

export default class User {
  constructor(private data: IUserProps) {}

  get(propName: string): string | number {
    return this.data[propName as keyof IUserProps];
  }
}

I found a blog that might be helpful to understand this better.

here is a link https://www.nadershamma.dev/blog/2019/how-to-access-object-properties-dynamically-using-bracket-notation-in-typescript/

Onesmus Muna
  • 411
  • 4
  • 5
12

When we do something like this obj[key] Typescript can't know for sure if that key exists in that object. What I did:

Object.entries(data).forEach(item => {
    formData.append(item[0], item[1]);
});
Alonad
  • 1,986
  • 19
  • 17
9

This might help someone First the empty object need to be with the type Record<string, any> so it will accept key as string and value as any

const sortFields: Record<string, any> = {};
const keyName = `field`;

sortFields[keyName] = 'asc';

So here when we log sortFields

 console.log("sortFields==== ", sortFields); // sortFields==== {field: 'asc'}

Or if we want to add more than one element in the object we can do it like this

sortFields.email = "asc"; // Or sortFields["email"] = "asc"; but its better written in dot notation.
sortFields.name = "desc";
sortFields.phone = "desc";

So now when we log sortFields

 console.log("sortFields==== ", sortFields); // sortFields==== {field: 'asc', email: 'asc', name: 'desc', phone: 'desc'}
Rooh Al-mahaba
  • 594
  • 1
  • 14
  • 28
8

With out typescript error

    const formData = new FormData();
    Object.keys(newCategory).forEach((k,i)=>{  
        var d =Object.values(newCategory)[i];
        formData.append(k,d) 
    })
Moumit
  • 8,314
  • 9
  • 55
  • 59
  • 3
    works fine, but i suggest to use forEach instead of map, cause a map usually returns something. – Cobi Jun 08 '21 at 10:56
7

Thanks to Alex Mckay I had a resolve for dynamic setting a props:

  for(let prop in filter)
      (state.filter as Record<string, any>)[prop] = filter[prop];
6

It worked for me with keyof and as operators:

const keys: [keyof ITrainInfo] = Object.keys(this.trainInfo) as [
    keyof ITrainInfo,
]
keys.forEach((property) => {
    // console.log(tmpUser[property])
    if (this.trainInfo === undefined) return
    if (this.trainInfo[property] !== undefined) {
        // your code here
        /*const trainsToSet = trains.find((field) => field.name === property)
        if (trainsToSet != undefined)
            trainsToSet.value = this.trainInfo[property]?.toString()
        */
    }
})
StepUp
  • 36,391
  • 15
  • 88
  • 148
6

as a last resort, you can mute this error by setting "suppressImplicitAnyIndexErrors": true in tsconfig.json

{
  "compilerOptions": {
    "suppressImplicitAnyIndexErrors": true,
  }
}
GorvGoyl
  • 42,508
  • 29
  • 229
  • 225
4

I made some small changes to Alex McKay's function/usage that I think make it a little easier to follow why it works and also adheres to the no-use-before-define rule.

First, define this function to use:

const getKeyValue = function<T extends object, U extends keyof T> (obj: T, key: U) { return obj[key] }

In the way I've written it, the generic for the function lists the object first, then the property on the object second (these can occur in any order, but if you specify U extends key of T before T extends object you break the no-use-before-define rule, and also it just makes sense to have the object first and its' property second. Finally, I've used the more common function syntax instead of the arrow operators (=>).

Anyways, with those modifications you can just use it like this:

interface User {
  name: string;
  age: number;
}

const user: User = {
  name: "John Smith",
  age: 20
};

getKeyValue(user, "name")

Which, again, I find to be a bit more readable.

1

TypeScript needs to be sure those values exist in trainInfo, else it reads all as string

instead of doing this

interface trainInfo {
  name: string;
  x: Array<number>;
  y: Array<number>;
  type: string;
  mode: string;
}

do this

interface trainInfo {
  name: "train_1" | "train_2" | "train_3"| "train_4";
  x: Array<number>;
  y: Array<number>;
  type: string;
  mode: string;
}
Chukwuemeka Maduekwe
  • 6,687
  • 5
  • 44
  • 67
1

In situation of nested object, if want for in in for in , here is a working example without error warning:

export const X = {
  aa: {
    a: '1',
    b: '2',
  },
  bb: {
    a: '3',
    b: '4',
  },
} as const

export const f1 = () => {
  let k1: keyof typeof X
  for (k1 in X) {
    console.log(k1)

    let k2: keyof (typeof X)[keyof typeof X]
    for (k2 in X[k1]) {
      console.log(X[k1][k2])
    }
  }
}
vikyd
  • 1,807
  • 1
  • 19
  • 28
0

This is not a answer to the original question, but a generic work around to this problem.

Original problem: person[cr.field] causes this error


I'm doing a generic advanced search form where user can select a field, comparator and the desired value. When trying to read the value from the object based on the key, I get this error (altought the field value is type of string and I think it should be just fine)

So what I do is I extract the [key, value] like this

const x: [string, any] = Object.entries(person).find(([key, _]) => key === cr.field);

For example if my criterion (cr) is { field: 'name', value: 'John' } and field name actually exists in a person obj., it should return the field name and the value as tuple (x is [string, any] or undef). If not found, undefined.

O-9
  • 1,626
  • 16
  • 15
0

public getUserName(): string {

const accessToken = this.getAccessToken();
const claims:any = this.getUserClaims();
console.log('access token ',accessToken);
this.getUserInfo();
return claims['sub'].split('@')[0];

}

//give any type to the variable

Nelcon Croos
  • 71
  • 1
  • 5
-1

I know it's a little too late, but all it's needed is to add a little type conversion, I wrote a static function that safely-returns the array of keys with the correct typing. All you need is to define the type and pass the object as a parameter:

export class ObjectUtil {
  public static getObjectKeys<T>(obj: Object) {
    if (!obj) {
      return [];
    }

    return Object.keys(obj).map((key: string) => key as keyof T);
  }
}

Below is a simple example:

ObjectUtil.getObjectKeys<Address>(address).forEach((key) => {
  console.log(address[key]);
});
dwjbosman
  • 906
  • 9
  • 33
-28

This is what it worked for me. The tsconfig.json has an option noImplicitAny that it was set to true, I just simply set it to false and now I can access properties in objects using strings.

Zeke
  • 140
  • 1
  • 10
  • 18
    This will remove strict, then there is no point in using typescript if we keep removing these restrictions. – Rip3rs Mar 12 '20 at 12:13
  • I don't agree @JosephBriggs. Typescript brings to the table so many other benefits. Type check is one of them. Is nice that you can opt-in or out depending on the requirements of your project. – Zeke Mar 16 '20 at 14:26
  • 34
    This does not solve the problem, it just ignores it. – thedayturns Mar 17 '20 at 21:27
  • 2
    @Zeke I understand mate :) I was in a rush writing. What I meant is that if we keep solving the issues by just telling it to ignore then there is no point of it in the first place. but then again all depends on the project, and the decisions per project. – Rip3rs Mar 24 '20 at 11:41
  • 2
    I prefer this... even strong type language like c# have "var". Refering https://stackoverflow.com/questions/62377614/element-implicitly-has-an-any-type-because-expression-of-type-string-cant-b ... A bit over engineered to put all sort of bumping code to access a simple property. – Eric Jun 22 '20 at 05:27
  • 1
    `var` in C# is not like `any` in Typescript. `var` will not work in a strongly typed language like `C#` if the type cannot be inferred, whereas `any` allows a variable to take on any type *at any time* - an `any` variable can be reassigned to a value of a completely different type. This is not possible in `C#`. – Joshua Wade Jun 25 '20 at 15:31
  • 1
    Even with `noImplicitAny`, you can still use `any`. You just can't do it *accidentally*, and the no-accidents guarantee one of the largest benefits of strong type checking. – Joshua Wade Jun 25 '20 at 15:33
  • You are telling the TypeScript that doesn't be itself – Hekmat Dec 07 '22 at 07:44