0

Take this example code:

interface Cat {
  fur: string
  food: string
}

const cats: Array<Cat> = [
  {
    fur: 'brown',
    food: 'fish'
  },
  {
    fur: 'white',
    food: 'biscuits'
  }
];

interface Cols {
  name: string
  title: string
}

const cols: Array<Cols> = [
  {
    name: 'fur',
    title: 'Fur colour'
  },
  {
    name: 'food',
    title: 'Favourite food'
  }
];

cats.map(cat => {
  cols.map(col => {
    console.log(`${col.title} - ${cat[col.name]}`); // errors here
  });
});

It errors on cat[col.name] with:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Cat'.
  No index signature with a parameter of type 'string' was found on type 'Cat'.(7053)

I can suppress that error by adding any to cat but that defeats the point. What's the correct way to type this?

1 Answer 1

2

This is because TypeScript sees col.name as a string, and the Cat interface only contains two possible keys (not any string as keys). In theory, col.name can return any string that is not present as a key in the object cat.

Since you know that col.name must be a keyof the Cat interface, you can hint TypeScript as such:

// Tell TS that col.name must be a keyof the Cat interface
cat[col.name as keyof Cat]

Therefore, updating your code like this will cause TypeScript to recognize that col.name indeed returns a valid key in the Cat interface:

cats.map(cat => {
  cols.map(col => {
    console.log(`${col.title} - ${cat[col.name as keyof Cat]}`);
  });
});

See proof-of-concept example on TypeScript Playground.

Pro-tip: let's say that you don't have an explicit interface Cat but only have the cat object, you can also do:

// Tell TS that col.name must be a keyof the typeof `cat` object
cat[col.name as keyof typeof cat]

An alternative way is to use enum for the properties in the Cat interface, so you don't have to manually cast it: since TypeScript can infer the type as such. This method is considerably more verbose at the expanse of not needed to manually cast the type of col.name (see Playground example here):

// Declare an enum for properties in `Cat` interface
enum CatProperty {
  FUR = 'fur',
  FOOD = 'food',
}

Then, when defining an array for your cols, you will need to use enums:

const cols: Array<Cols> = [
  {
    name: CatProperty.FUR,
    title: 'Fur colour'
  }
];

This should result in TypeScript being able to recognize that col.name must be a value of fur | food, which all exists in the Cat interface, so no casting is needed:

cats.map(cat => {
  cols.map(col => {
    console.log(`${col.title} - ${cat[col.name]}`);
  });
});
Sign up to request clarification or add additional context in comments.

2 Comments

That's brilliant! Thanks so much for the answer & taking the time to explain it so well :-) ....I'm pretty new to Typescript and the kindness of random strangers like you may just stop me pulling my hair out ;-) You made my day!
@BaronVonKaneHoffen Glad to be able to help :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.