Skip to content

TypeScript з Композиційним API

Ця сторінка передбачає, що ви вже ознайомились із Використання Vue з TypeScript.

Типізація реквізитів компонента

Використання <script setup>

При використанні <script setup>, макрос defineProps() підтримує визначення типів реквізитів на основі свого аргументу:

vue
<script setup lang="ts">
const props = defineProps({
  foo: { type: String, required: true },
  bar: Number
})

props.foo // string
props.bar // number | undefined
</script>

Це називається «декларація під час виконання», тому що аргумент, переданий у defineProps(), використовуватиметься як параметр props під час виконання.

Однак, зазвичай простіше визначити реквізити з базовими типами, за допомогою аргументу узагальнення:

vue
<script setup lang="ts">
const props = defineProps<{
  foo: string
  bar?: number
}>()
</script>

Це називається «декларація на основі типу». Компілятор намагатиметься зробити все можливе, щоб визначити еквівалентні параметри під час виконання, на основі типу аргументу. У цьому випадку наш другий приклад компілюється в ті самі параметри під час виконання, що й перший приклад.

Ви можете використовувати «декларацію на основі типу», АБО «декларацію під час виконання», але ви не можете використовувати обидва одночасно.

Ми також можемо винести типи реквізитів в окремий інтерфейс:

vue
<script setup lang="ts">
interface Props {
  foo: string
  bar?: number
}

const props = defineProps<Props>()
</script>

Синтаксичні обмеження

У версії 3.2 і нижче параметр загального типу для defineProps() обмежувався літералом типу або посиланням на локальний інтерфейс.

Це обмеження було вирішено у 3.3. Остання версія Vue підтримує посилання на імпортовані та обмежений набір складних типів у позиції параметра типу. Однак, оскільки перетворення типу під час виконання все ще базується на AST, деякі складні типи, які вимагають фактичного аналізу типу, наприклад умовні типи, не підтримуються. Ви можете використовувати умовні типи для типу окремого реквізиту, але не для всього об'єкта реквізитів.

Значення за замовчуванням для реквізитів

При використанні декларації на основі типу, для реквізитів ми втрачаємо можливість оголошувати значення за замовчуванням. Це можна вирішити макросом withDefaults:

ts
export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'привіт',
  labels: () => ['один', 'два']
})

Це буде скомпільовано до еквівалентних параметрів реквізитів default. Крім того, помічник withDefaults забезпечує перевірку типу для значень за замовчуванням і гарантує, що повернутий тип props має видалені додаткові прапорці для властивостей, які мають оголошені значення за замовчуванням.

Без <script setup>

Якщо не використовувати <script setup>, то необхідно використовувати defineComponent() щоб увімкнути визначення типу реквізитів. Тип реквізитів, який переданий до setup(), визначається з параметру props.

ts
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    message: String
  },
  setup(props) {
    props.message // <-- тип: string
  }
})

Складні типи реквізитів

За допомогою декларації на основі типу, реквізит може використовувати складний тип, як і будь-який інший тип:

vue
<script setup lang="ts">
interface Book {
  title: string
  author: string
  year: number
}

const props = defineProps<{
  book: Book
}>()
</script>

Для декларації під час виконання ми можемо використовувати утилітарний тип PropType:

ts
import type { PropType } from 'vue'

const props = defineProps({
  book: Object as PropType<Book>
})

Це працює приблизно так само, якщо ми безпосередньо вказуємо опцію props:

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

export default defineComponent({
  props: {
    book: Object as PropType<Book>
  }
})

Параметр props частіше використовується з Options API, тому ви знайдете докладніші приклади в посібнику TypeScript with Options API . Техніки, показані в цих прикладах, також застосовуються до декларацій під час виконання за допомогою defineProps().

Типізація випромінювань компонента

В <script setup>, emit- функцію також можна типізувати за допомогою, декларації під час виконання, АБО декларації на основі типу:

vue
<script setup lang="ts">
// під час виконання
const emit = defineEmits(['change', 'update'])

// на основі типу
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

// 3.3+: альтернативний, більш стислий синтаксис
const emit = defineEmits<{
  change: [id: number]
  update: [value: string]
}>()
</script>

Аргумент типу може бути одним із таких:

  1. Функцією, яка написана як літерал типу з Сигнатурою виклику. Його буде використано як тип повернутої функції emit.
  2. Літерал типу, де ключами є імена подій, а значеннями є типи масивів/кортежів, що представляють додаткові прийнятні параметри для події. У наведеному вище прикладі використовуються іменовані кортежі, тому кожен аргумент може мати явне ім’я.

Як ми бачимо, оголошення типу дає нам набагато більш детальний контроль над обмеженнями типу випущених подій.

Якщо не використовувати <script setup>, defineComponent() може визначити дозволені події для функції emit, представленої в контексті setup:

ts
import { defineComponent } from 'vue'

export default defineComponent({
  emits: ['change'],
  setup(props, { emit }) {
    emit('change') // <-- перевірка типу / автодоповнення
  }
})

Типізація ref()

Тип референції визначається із початкового значення:

ts
import { ref } from 'vue'

// визначений тип: Ref<number>
const year = ref(2020)

// => TS Помилка: тип "string" не можна призначити типу "number".
year.value = '2020'

Іноді нам може знадобитися вказати складні типи для внутрішніх значень референції. Ми можемо зробити це за допомогою типу Ref:

ts
import { ref } from 'vue'
import type { Ref } from 'vue'

const year: Ref<string | number> = ref('2020')

year.value = 2020 // ok!

Або передаючи аргумент узагальнення під час виклику ref(), щоб замінити визначення за замовчуванням:

ts
// отриманий тип: Ref<string | number>
const year = ref<string | number>('2020')

year.value = 2020 // ok!

Якщо ви вкажете аргумент узагальнення, але опустите початкове значення, то результатом буде об'єднаний тип, який включає undefined:

ts
// визначений тип: Ref<number | undefined>
const n = ref<number>()

Типізація reactive()

reactive() також неявно визначає тип зі свого аргументу:

ts
import { reactive } from 'vue'

// inferred type: { title: string }
const book = reactive({ title: 'Vue 3 Guide' })

Щоб явно встановити тип reactive властивості, ми можемо використовувати інтерфейси:

ts
import { reactive } from 'vue'

interface Book {
  title: string
  year?: number
}

const book: Book = reactive({ title: 'Vue 3 Guide' })

TIP

Не рекомендується використовувати аргумент узагальнення reactive(), оскільки при обробці він розвертає вкладені референції й тип при повернені буде відрізнятися від аргументу узагальнення.

Типізація computed()

computed() визначає свій тип на основі значення, яке повертає гетер:

ts
import { ref, computed } from 'vue'

const count = ref(0)

// визначений тип: ComputedRef<number>
const double = computed(() => count.value * 2)

// => TS Помилка: Властивість 'split' не є типом "number"
const result = double.value.split('')

Ви також можете вказати, явний тип, за допомогою аргументу узагальнення:

ts
const double = computed<number>(() => {
  // помилка типу, якщо не повертається число
})

Типізація обробників подій

При роботі з нативними подіями DOM, буде корисним правильно типізувати аргумент, який ми передаємо обробнику. Розгляньмо це на прикладі:

vue
<script setup lang="ts">
function handleChange(event) {
  // `event` неявно має тип `any`
  console.log(event.target.value)
}
</script>

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

Без анотації типу, аргумент event неявно матиме тип any. Це також призведе до помилки TS, якщо "strict": true, або "noImplicitAny": true вказані в tsconfig.json. Тому рекомендується явно анотувати аргументи обробників подій. Тому ми радимо вам явно задавати типи аргументів для обробників подій. Крім того, можливо, вам потрібно буде ще задавати типи для властивостей об'єкта event:

ts
function handleChange(event: Event) {
  console.log((event.target as HTMLInputElement).value)
}

Типізація Provide/Inject

Provide та Inject зазвичай виконуються в окремих компонентах. Щоб правильно типізувати значення Inject, Vue надає інтерфейс InjectionKey, який є універсальним типом і розширює Symbol. Його можна використовувати для синхронізації типу введеного значення між постачальником і споживачем:

ts
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>

provide(key, 'foo') // надання не string значення призведе до помилки

const foo = inject(key) // тип foo: string | undefined

Рекомендовано розмістити ключ ін'єкції в окремому файлі, щоб його можна було імпортувати в кількох компонентах.

Під час використання string ключів ін'єкції, тип введеного значення буде unknown, і його потрібно явно оголосити за допомогою аргументу узагальнення:

ts
const foo = inject<string>('foo') // тип: string | undefined

Зауважте, що введене значення все ще може бути undefined, оскільки немає гарантії, що постачальник надасть це значення під час виконання.

Тип undefined можна видалити, вказавши значення за замовчуванням:

ts
const foo = inject<string>('foo', 'bar') // тип: string

Якщо ви впевнені, що значення завжди надається, ви також можете примусово привести значення:

ts
const foo = inject('foo') as string

Типізація референцій в шаблонах

Референції в шаблонах мають бути створені з явним аргументом узагальнення, та початковим значенням null:

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const el = ref<HTMLInputElement | null>(null)

onMounted(() => {
  el.value?.focus()
})
</script>

<template>
  <input ref="el" />
</template>

Щоб отримати правильний інтерфейс DOM, ви можете переглянути такі сторінки, як MDN.

Зверніть увагу, що для досягнення суворішої перевірки типу, необхідно використовувати додаткове зв'язування, або захистити тип під час доступу до el.value. Це пояснюється тим, що початкове значення референції дорівнює null, доки компонент не буде змонтовано. і вона також буде встановлена в null, якщо елемент, на який вона посилається, демонтується за допомогою v-if.

Типізація референцій компонентів у шаблонах

Іноді вам може знадобитися анотувати шаблон референції для дочірнього компонента, щоб викликати його публічний метод. Наприклад, у нас є дочірній компонент MyModal з методом, який відкриває модальне вікно:

vue
<!-- MyModal.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const isContentShown = ref(false)
const open = () => (isContentShown.value = true)

defineExpose({
  open
})
</script>

Щоб отримати екземпляр типу MyModal, нам потрібно спочатку отримати його тип за допомогою typeof, а потім використати вбудовану утиліту TypeScript InstanceType, щоб отримати його тип:

vue
<!-- App.vue -->
<script setup lang="ts">
import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () => {
  modal.value?.open()
}
</script>

Зверніть увагу, якщо ви хочете використовувати цю техніку у файлах TypeScript, замість одно-файлових компонент, вам потрібно ввімкнути Режим Takeover у Volar.

У випадках, коли точний тип компонента недоступний або неважливий, замість нього можна використовувати ComponentPublicInstance. Це включатиме лише властивості, які є спільними для всіх компонентів, наприклад $el:

ts
import { ref } from 'vue'
import type { ComponentPublicInstance } from 'vue'

const child = ref<ComponentPublicInstance | null>(null)
TypeScript з Композиційним API has loaded