'TypeScript error ts(2345) is incorrectly labeling the type of object in Vue v-for loop

I am using Vue single file components. Here is a small example showing my error:

<script setup lang="ts">

interface IDesign {
  displacement_map_urls: { [key: string]: string }
}

const props = defineProps<{
  design: IDesign
}>()

// function to convert string to title case
const toTitleCase = (str:string) => {
  return str.replace(/\w\S*/g, function (txt:string): string { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() })
}

</script>


<template>
  <div >
    <div v-for="(url, face ) in props.design.displacement_map_urls">
      <span>{{ toTitleCase(face)}}</span>
      <span>{{ url }}</span>
    </div>
  </div>
</template>

Visual Studio Code underlines face in the template with this error:

const face: string | number
Argument of type 'string | number' is not assignable to parameter of type 'string'.
  Type 'number' is not assignable to type 'string'.ts(2345)

For some reason ts thinks that the face variable is a string|number, however if you look at the interface IDesign you can see that the displacement_map_urls is an object with string keys and string values.

I think this has something to do with the Vue v-for loop, because if I manually grab the first key without going through a v-for loop then VSCode does not show an error:

{{ toTitleCase(Object.keys(props.design.displacement_map_urls)[0]) }}

Please let me know if I need to provide anything else to help solve this problem.



Solution 1:[1]

face is the index into displacement_maps_urls. It is type string | number because displacement_maps_urls could be an array (or an integer-keyed object). I think the parser isn't smart enough to always get the types of the Vue template properties right.

string | number cannot be automatically cast to string because sometimes it's a number. But since in your case you know it's always a string, you can just cast it manually (face as string):

<template>
  <div >
    <div v-for="(url, face ) in props.design.displacement_map_urls">
      <span>{{ toTitleCase(face as string)}}</span>
      <span>{{ url }}</span>
    </div>
  </div>
</template>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Dov Rosenberg