typescriptvue.jsgenericsvuejs3

why is my generic v-for key giving a type error in Vue 3.3?


My app uses Vue 3.3.4 which added support for generics in single file components. My component is a generic list that loops over a set of items passed as a prop. Additionally, there is an itemKey prop for keying the v-for loop.

<script setup lang="ts" generic="T, K extends keyof T">
defineProps<{
  items: Array<T>
  itemKey: K
}>()
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item[itemKey]">
      <slot :item="item" />
    </li>
  </ul>
</template>

The :key="item[itemKey]" gets highlighted and the following error is shown

Type '[{ type: PropType<K>; required: true; }] extends [Prop<infer V, infer D>] ? unknown extends V ? IfAny<V, V, D> : V : { type: PropType<K>; required: true; }' cannot be used to index type 'T'.

It should be noted that this is only a type error; the code itself runs.

Why is my itemKey being misinterpreted?


TEMPORARY SOLUTION: Use an inline //@ts-ignore in the :key prop.

<li
  v-for="(item, i) in items"
  :key="
    //@ts-ignore
    item[itemKey]
  "
>

Solution

  • Reflection to your code

    original code

    This is because the type of itemKey is generic in the component definition and is only known at runtime. Therefore, the static type checking of TypeScript cannot determine exactly which key it refers to.

    It is necessary to provide a more specific type definition for items[].

    type of :key on vue

    In Vue, the :key attribute expects string, number, or symbol values. Therefore, if the type of item[itemKey] matches one of these types, we use it as the key otherwise, we don't.


    Solution # 1 - T extends U ? U : never - TypeScript 2.8 and above

    In this case, we assume that every value in the object T can be a valid key. After all, by using item[itemKey], we essentially made this assertion, which is causing the error.

    Array<{ [key in keyof T]: T[key] extends string | number | symbol ? T[key] : never }>
    

    The items prop is an array where, based on the keys (keyof T) of the T type, it selects the types from T[key] that are string, number, or symbol. If the types are not one of these, it returns the never type.

    This ensures that only string, number, or symbol values can be assigned to the :key attribute.

    <script setup lang="ts" generic="T, K extends keyof T">
    defineProps<{
      items: Array<{ [key in keyof T]: T[key] extends string | number | symbol ? T[key] : never }>
      itemKey: K
    }>()
    </script>
    
    <template>
      <ul>
        <li v-for="item in items" :key="item[itemKey]">
          <slot :item="item" />
        </li>
      </ul>
    </template>
    



    Solution # 2 (declare item[itemKey] type)

    If the object can contain values of different types, it may not be worth the effort to perform precise type checking. It is advisable to declare `item[itemKey] as a value that meets our expectations for later use. Additionally, you may consider writing runtime JavaScript checks to verify the type of item[itemKey], allowing you to display an error message in the console as an error during development mode.

    <script setup lang="ts" generic="T, K extends keyof T">
    defineProps<{
      items: Array<T>
      itemKey: K
    }>()
    </script>
    
    <template>
      <ul>
        <li v-for="item in items" :key="(item[itemKey as K] as string | number | symbol)">
          <slot :item="item" />
        </li>
      </ul>
    </template>
    



    Solution # 3 (convert to string)

    If you want to ensure that the :key attribute receives a string value, you can simply convert the item[itemKey] value to a string. By doing so, TypeScript won't raise any issues because regardless of the actual type of item[itemKey], you have explicitly converted it to a string, which satisfies the requirements of the :key attribute.

    <script setup lang="ts" generic="T, K extends keyof T">
    defineProps<{
      items: Array<T>
      itemKey: K
    }>()
    </script>
    
    <template>
      <ul>
        <li v-for="item in items" :key="String(item[itemKey as K])">
          <slot :item="item" />
        </li>
      </ul>
    </template>