blog

Vue PropType으로 props 타입 좁히기

Front-end / Vue#Vue#TypeScript#PropType#props

런타임 props 선언(props: { status: String })만으로는 TypeScript가 string, object, unknown[] 같은 넓은 타입밖에 알 수 없다. PropType<T>는 이 런타임 생성자에 TypeScript 타입을 덧씌워 더 구체적인 타입을 지정한다.

기본 사용법

import { defineComponent, type PropType } from 'vue';

type Status = 'success' | 'error' | 'warning';

export default defineComponent({
  props: {
    status: String as PropType<Status>,
  },
});

status의 타입이 string이 아니라 'success' | 'error' | 'warning'으로 추론된다. 부모가 status="hello"를 넘기면 템플릿 타입 검사(vue-tsc, Volar)에서 에러가 난다.

다만 런타임 검사는 여전히 String 이다. 타입 검사 없이 실행되는 환경에서 잘못된 값이 들어와도 경고가 나지 않는다. 런타임에서도 막으려면 validator를 함께 쓴다.

props: {
  status: {
    type: String as PropType<Status>,
    required: true,
    validator: (value: string) => ['success', 'error', 'warning'].includes(value),
  },
},

객체·배열 props

interface User {
  id: number;
  name: string;
}

props: {
  user: Object as PropType<User>,
  items: Array as PropType<string[]>,
  onSelect: Function as PropType<(id: number) => void>,
},

Object만 쓰면 Record<string, any>, Array만 쓰면 unknown[]로 추론되어 자동완성과 검사를 받을 수 없다.

객체·배열의 기본값은 팩토리 함수로

props: {
  user: {
    type: Object as PropType<User>,
    default: () => ({ id: 0, name: 'Guest' }),
  },
  items: {
    type: Array as PropType<string[]>,
    default: () => [],
  },
},

객체·배열 기본값은 반드시 함수로 반환해야 한다. 함수는 컴포넌트 인스턴스마다 호출되어 새 객체를 만든다. 객체를 직접 넣으면 모든 인스턴스가 같은 객체를 공유하게 되므로 Vue가 이를 허용하지 않는다.

<script setup>에서는 타입 기반 선언

<script setup lang="ts">라면 PropType 없이 타입으로 바로 선언하는 편이 간결하다.

<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    status: 'success' | 'error' | 'warning';
    user?: User;
    items?: string[];
  }>(),
  {
    items: () => [],
  },
);
</script>

Vue 3.5 이상에서는 const { items = [] } = defineProps<...>()처럼 구조 분해 기본값도 사용할 수 있다. PropType은 Options API나 defineComponent의 런타임 선언에서 필요하다.