티스토리 뷰
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
- 스파르타 코딩클럽
- oracle 19c 설치
- unmounted setinterval
- 강서 운전면허 시험장
- rest api 조회 생성 수정 삭제
- unplugin-auto-import
- vue watch 대체
- vue watch 위험성
- Oracle Database 19C 설치
- dockerignore
- 외래키 삭제
- 1종 적성검사
- docker multi stage build
- 1종 적성검사 국가건강검진
- unmounted document.addlistener
- 1종 적성검사 신체검사
- vue 이벤트 해제
- vue onunmounted
- rest api 단건 다건
- vue unmounted
- 오블완
- 스마트피싱보호_캠페인
- unmounted composable
- 1종 적성검사 과태료
- 티스토리챌린지
- vue watch 문제점
- vue 리팩토링
- docker image 경량화
- Oracle Database 19c install
- vue 타이머 해제
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |