Skip to content

옵션 API에서 TypeScript 사용하기

이 페이지는 TypeScript와 함께 Vue 사용하기 개요를 이미 읽었다고 가정합니다.

TIP

Vue는 옵션 API와 함께 TypeScript 사용을 지원하지만, 더 간단하고 효율적이며 견고한 타입 추론을 제공하는 컴포지션 API를 통해 TypeScript와 함께 Vue를 사용하는 것이 권장됩니다.

컴포넌트 Props 타입 지정하기

옵션 API에서 props의 타입 추론을 위해서는 컴포넌트를 defineComponent()로 감싸야 합니다. 이를 통해 Vue는 props 옵션을 기반으로, required: truedefault와 같은 추가 옵션을 고려하여 props의 타입을 추론할 수 있습니다:

ts
import { defineComponent } from 'vue'

export default defineComponent({
  // 타입 추론 활성화
  props: {
    name: String,
    id: [Number, String],
    msg: { type: String, required: true },
    metadata: null
  },
  mounted() {
    this.name // 타입: string | undefined
    this.id // 타입: number | string | undefined
    this.msg // 타입: string
    this.metadata // 타입: any
  }
})

하지만 런타임 props 옵션에서는 생성자 함수만을 prop의 타입으로 사용할 수 있습니다. 중첩된 속성을 가진 객체나 함수 호출 시그니처와 같은 복잡한 타입을 지정할 방법은 없습니다.

복잡한 props 타입을 명시하려면 PropType 유틸리티 타입을 사용할 수 있습니다:

ts
import { defineComponent } from 'vue'
import type { PropType } from 'vue'

interface Book {
  title: string
  author: string
  year: number
}

export default defineComponent({
  props: {
    book: {
      // `Object`에 더 구체적인 타입 제공
      type: Object as PropType<Book>,
      required: true
    },
    // 함수도 타입 지정 가능
    callback: Function as PropType<(id: number) => void>
  },
  mounted() {
    this.book.title // string
    this.book.year // number

    // TS 오류: 'string' 타입의 인수는
    // 'number' 타입의 매개변수에 할당할 수 없음
    this.callback?.('123')
  }
})

주의사항

TypeScript 버전이 4.7 미만인 경우, validatordefault prop 옵션에 함수 값을 사용할 때 주의해야 합니다. 반드시 화살표 함수를 사용하세요:

ts
import { defineComponent } from 'vue'
import type { PropType } from 'vue'

interface Book {
  title: string
  year?: number
}

export default defineComponent({
  props: {
    bookA: {
      type: Object as PropType<Book>,
      // TypeScript 4.7 미만에서는 반드시 화살표 함수 사용
      default: () => ({
        title: 'Arrow Function Expression'
      }),
      validator: (book: Book) => !!book.title
    }
  }
})

이렇게 하면 이러한 함수 내부에서 this의 타입을 TypeScript가 추론해야 하는 상황을 방지할 수 있습니다. 이는 이전의 설계 제한이었으며, TypeScript 4.7에서 개선되었습니다.

컴포넌트 Emits 타입 지정하기

emits 옵션의 객체 문법을 사용하여 발생시킬 이벤트의 예상 페이로드 타입을 선언할 수 있습니다. 또한, 선언되지 않은 모든 이벤트를 발생시키면 타입 오류가 발생합니다:

ts
import { defineComponent } from 'vue'

export default defineComponent({
  emits: {
    addBook(payload: { bookName: string }) {
      // 런타임 유효성 검사 수행
      return payload.bookName.length > 0
    }
  },
  methods: {
    onSubmit() {
      this.$emit('addBook', {
        bookName: 123 // 타입 오류!
      })

      this.$emit('non-declared-event') // 타입 오류!
    }
  }
})

계산된 속성 타입 지정하기

계산된 속성은 반환값을 기반으로 타입을 추론합니다:

ts
import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return {
      message: 'Hello!'
    }
  },
  computed: {
    greeting() {
      return this.message + '!'
    }
  },
  mounted() {
    this.greeting // 타입: string
  }
})

경우에 따라 계산된 속성의 타입을 명시적으로 지정하여 구현이 올바른지 보장하고 싶을 수 있습니다:

ts
import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return {
      message: 'Hello!'
    }
  },
  computed: {
    // 반환 타입을 명시적으로 지정
    greeting(): string {
      return this.message + '!'
    },

    // 쓰기 가능한 계산된 속성에 타입 지정
    greetingUppercased: {
      get(): string {
        return this.greeting.toUpperCase()
      },
      set(newValue: string) {
        this.message = newValue.toUpperCase()
      }
    }
  }
})

명시적 타입 지정은 TypeScript가 순환 추론 루프로 인해 계산된 속성의 타입을 추론하지 못하는 일부 예외적인 경우에도 필요할 수 있습니다.

이벤트 핸들러 타입 지정하기

네이티브 DOM 이벤트를 다룰 때, 핸들러에 전달하는 인자의 타입을 올바르게 지정하는 것이 유용할 수 있습니다. 다음 예제를 살펴봅시다:

vue
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  methods: {
    handleChange(event) {
      // `event`는 암시적으로 `any` 타입을 가짐
      console.log(event.target.value)
    }
  }
})
</script>

<template>
  <input type="text" @change="handleChange" />
</template>

타입 지정을 하지 않으면 event 인자는 암시적으로 any 타입을 갖게 됩니다. 이는 tsconfig.json에서 "strict": true 또는 "noImplicitAny": true가 사용될 경우 TS 오류로 이어집니다. 따라서 이벤트 핸들러의 인자를 명시적으로 타입 지정하는 것이 권장됩니다. 또한, event의 속성에 접근할 때 타입 단언을 사용해야 할 수도 있습니다:

ts
import { defineComponent } from 'vue'

export default defineComponent({
  methods: {
    handleChange(event: Event) {
      console.log((event.target as HTMLInputElement).value)
    }
  }
})

전역 속성 확장하기

일부 플러그인은 app.config.globalProperties를 통해 모든 컴포넌트 인스턴스에 전역적으로 사용할 수 있는 속성을 설치합니다. 예를 들어, 데이터 패칭을 위한 this.$http나 국제화를 위한 this.$translate를 설치할 수 있습니다. TypeScript와 잘 연동되도록, Vue는 TypeScript 모듈 보강을 통해 확장할 수 있도록 설계된 ComponentCustomProperties 인터페이스를 제공합니다:

ts
import axios from 'axios'

declare module 'vue' {
  interface ComponentCustomProperties {
    $http: typeof axios
    $translate: (key: string) => string
  }
}

참고:

타입 보강 위치

이 타입 보강은 .ts 파일이나 프로젝트 전체에 적용되는 *.d.ts 파일에 둘 수 있습니다. 어느 쪽이든 tsconfig.json에 포함되어야 합니다. 라이브러리/플러그인 작성자의 경우, 이 파일을 package.jsontypes 속성에 지정해야 합니다.

모듈 보강을 활용하려면, 보강이 TypeScript 모듈 내에 위치해야 합니다. 즉, 파일에 최상위 importexport가 하나 이상 있어야 하며, 단순히 export {}만 있어도 됩니다. 모듈 외부에 보강을 두면 원래 타입을 덮어써버리므로 주의하세요!

ts
// 동작하지 않으며, 원래 타입을 덮어씁니다.
declare module 'vue' {
  interface ComponentCustomProperties {
    $translate: (key: string) => string
  }
}
ts
// 올바르게 동작함
export {}

declare module 'vue' {
  interface ComponentCustomProperties {
    $translate: (key: string) => string
  }
}

커스텀 옵션 확장하기

일부 플러그인(예: vue-router)은 beforeRouteEnter와 같은 커스텀 컴포넌트 옵션을 지원합니다:

ts
import { defineComponent } from 'vue'

export default defineComponent({
  beforeRouteEnter(to, from, next) {
    // ...
  }
})

적절한 타입 보강이 없으면 이 훅의 인자들은 암시적으로 any 타입을 갖게 됩니다. 이러한 커스텀 옵션을 지원하려면 ComponentCustomOptions 인터페이스를 확장할 수 있습니다:

ts
import { Route } from 'vue-router'

declare module 'vue' {
  interface ComponentCustomOptions {
    beforeRouteEnter?(to: Route, from: Route, next: () => void): void
  }
}

이제 beforeRouteEnter 옵션이 올바르게 타입 지정됩니다. 이는 단순한 예시일 뿐이며, vue-router와 같이 타입이 잘 지정된 라이브러리는 자체 타입 정의에서 이러한 보강을 자동으로 수행해야 합니다.

이 보강의 위치는 전역 속성 보강과 동일한 제한을 받습니다.

참고:

옵션 API에서 TypeScript 사용하기 has loaded