티스토리 뷰
Vue Generic에서 keyof T 안전하게 사용하는 법 (v-for의 key 문제 해결)
Vue에서 Generic 컴포넌트를 만들다 보면 object[]를 Props로 받아서 순환하게 되는 경우가 종종있다.
이때 각 항목을 고유하게 식별하기 위해 v-for의 :key에 item[itemKey]와 같이 접근하게 되는데, 이런 코드에서 아래와 같은 TypeScript 오류를 마주칠 수 있다.
⚠️ 문제 상황
<script setup lang="ts" generic="T extends object">
interface Props<T extends object> {
    items: T[]
    itemKey: keyof T
}
const props = defineProps<Props<T>>()
</script>
<template>
    <div>
        <span v-for="item in items" :key="item[itemKey]"></span>
    </div>
</template>
위 코드에서 아래와 같은 에러가 발생.
Type '[{ type: PropType<keyof T>; required: true; }]' extends [Prop<infer V, infer D>] ? unknown extends V ? IfAny<V, V, D> : V : { type: PropType<keyof T>; required: true; }' cannot be used to index type 'T'.ts(2536)
📌 왜 이런 문제가 발생할까?
이 문제는 template 내부에서 TypeScript가 generic한 key 접근을 타입 안정적으로 보장할 수 없기 때문.
setup()내부에서는item[props.itemKey]접근이 정상 작동.- 그러나 
template내부에서는itemKey의 타입이keyof T라고 해도,T[itemKey]가 PropertyKey가 아닌 불확정 타입으로 간주됩니다. 
즉, 런타임에서 안전하게 v-for의 key로 사용할 수 있을지 컴파일러가 확정할 수 없다는것.
✅ 최초 해결 시도
StackOverflow에서는 아래와 같이 제안하였다.
defineProps<{
  items: Array<{ [key in keyof T]: T[key] extends PropertyKey ? T[key] : never }>
  itemKey: keyof T
}>()
즉, items 배열을 Mapped Type + 조건부 타입을 활용해서 모든 속성이 PropertyKey일 때만 받도록 제한하는 방식.
하지만 이 방식은 다음과 같은 단점이 존재하였다.
const items = ref([
  { id: 1, date: new Date() },
  { id: 2, date: new Date() },
])
위처럼 Date 타입이 포함된 객체는 허용되지 않는다.
왜냐하면 Date는 PropertyKey가 아니기 때문..
의도했던건 단지 key로 사용할 필드만 PropertyKey면 되는 것인데 말이다.
💡 안전하고 유연한 해결 방법
1. setup() 영역에서 getItemKey() 함수 정의
function getItemKey(item: T) {
  return item[props.itemKey] as PropertyKey;
}
2. template에서는 이렇게 사용
<span v-for="item in items" :key="getItemKey(item)"></span>
as PropertyKey로 타입 단언하여 Vue가 받아들이도록 함- Vue template 내부에서는 타입 추론이 불완전하므로 
setup()내부에서 제어하는 것이 더 안전 
🔐 유틸 타입으로 itemKey를 안전하게 제한하기
type ValidObjectKey<T> = {
  [K in keyof T]: T[K] extends PropertyKey ? K : never
}[keyof T]
이 타입은 다음과 같은 방식으로 동작.
type MyObj = {
  a: string
  b: number
  c: () => void
  d: object
}
type Result = { [K in keyof MyObj]: MyObj[K] extends PropertyKey ? K : never }
// 결과 타입 👇
// { a: "a"; b: "b"; c: never; d: never; }
type Result2 = Result[keyof MyObj]
// 결과 타입 👇
// "a" | "b"
즉, 객체의 key 중에서 value가 PropertyKey (string | number | symbol) 인 key만 추려내는 유틸 타입이다.
조건부 타입으로 never 타입이 된 key들은 indexed Acces Type으로 접근시 제외되어 위와 같은 결과가 나온다.
🎯 최종 정리된 코드
<script setup lang="ts" generic="T extends object">
interface Props<T extends object> {
  items: T[]
  itemKey: ValidObjectKey<T>
}
const props = defineProps<Props<T>>()
function getItemKey(item: T) {
  return item[props.itemKey] as PropertyKey
}
</script>
<template>
  <div>
    <span v-for="item in items" :key="getItemKey(item)">
      {{ item[props.itemKey] }}
    </span>
  </div>
</template>
ValidObjectKey<T>를 통해itemKey의 값이PropertyKey인 키만 허용setup()내에서getItemKey()를 정의하고 타입 단언하여template에서 안전하게 사용
🏁 마치며
그동안 template 내부에서 generic key 접근을 타입 단언으로 임시방편 처리했었다면, 이번 기회에 Mapped Type, Conditional Type, Indexed Access Type을 활용해 정확하고 안전하게 제약을 거는 방법을 익힐 수 있는 
좋은 기회가 되었다.
이 글이 Vue + TypeScript를 사용하는 다른 개발자분들에게도 도움이 되었길 바랍니다 😄
- Total
 
- Today
 
- Yesterday
 
- unmounted setinterval
 - nuxt3 structure
 - unmounted composable
 - 외래키 삭제
 - Oracle Database 19C 설치
 - oracle 19c 설치
 - docker multi stage build
 - 티스토리챌린지
 - docker mssql 이미지 생성
 - 오블완
 - 스파르타 코딩클럽
 - unmounted document.addlistener
 - vue 리팩토링
 - nuxt3 eslint prettier 설정
 - nuxt3 프로젝트 설정
 - dockerignore
 - docker image 경량화
 - Oracle Database 19c install
 - vue onunmounted
 - docker mssql create database
 - vue watch 대체
 - vue 이벤트 해제
 - 스마트피싱보호_캠페인
 - vue unmounted
 - vue watch 문제점
 - vue watch 위험성
 - nuxt3 quasar 설정
 - unplugin-auto-import
 - vue 타이머 해제
 - docker mssql
 
| 일 | 월 | 화 | 수 | 목 | 금 | 토 | 
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 
| 9 | 10 | 11 | 12 | 13 | 14 | 15 | 
| 16 | 17 | 18 | 19 | 20 | 21 | 22 | 
| 23 | 24 | 25 | 26 | 27 | 28 | 29 | 
| 30 | 
