TypeScript с Composition 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>Это также работает, если Props импортируется из внешнего файла. Эта функция требует, чтобы TypeScript был зависимостью от Vue.
vue
<script setup lang="ts">
import type { Props } from './foo'
const props = defineProps<Props>()
</script>Ограничения Синтаксиса
В версии 3.2 и ниже параметр дженерика для defineProps() был ограничен литеральным типом или ссылкой на интерфейс в этом же файле.
Это ограничение было устранено в версии 3.3. Последняя версия Vue поддерживает ссылки на импортируемые и ограниченный набор сложных типов в позиции параметра типа. Однако, поскольку преобразование типов во время выполнения все еще основано на AST, некоторые сложные типы, требующие фактического анализа типов, например условные типы, не поддерживаются. Вы можете использовать условные типы для типа отдельного входного параметра, но не всего объекта входных параметров.
Значения по умолчанию входных параметров
При объявлении на основе типов мы теряем возможность задавать значения по умолчанию для входных параметров. Это решается с помощью реактивной деструктуризации входных параметров :
ts
interface Props {
msg?: string
labels?: string[]
}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()В версиях 3.4 и ниже реактивная деструктуризация входных параметров по умолчанию отключена. Альтернатива — макрос компилятора withDefaults:
ts
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'привет',
labels: () => ['один', 'два']
})Это будет скомпилировано во время выполнения в эквивалентные параметры по умолчанию. Кроме того, помощник withDefaults обеспечивает проверку типов для значений по умолчанию и гарантирует, что в возвращаемом типе props будут удалены необязательные флаги для свойств, для которых объявлены значения по умолчанию.
INFO
Обратите внимание: значения по умолчанию для изменяемых ссылочных типов (массивы, объекты) при использовании withDefaults следует оборачивать в функции, чтобы избежать случайного изменения и побочных эффектов. Так каждый экземпляр компонента получит свою копию значения по умолчанию. При использовании значений по умолчанию с деструктуризацией это не требуется.
Без <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 с Options API. Приемы, показанные в этих примерах, также применимы к объявлениям в рантайме с помощью defineProps().
Типизация испускаемых событий компонента
В <script setup>, функция emit также может быть типизирована с помощью определения в рантайме, либо определения через тип:
vue
<script setup lang="ts">
// в рантайме
const emit = defineEmits(['change', 'update'])
// основанный на опциях
const emit = defineEmits({
change: (id: number) => {
// верните `true` или `false` чтобы обозначить
// валидация пройдена / провалена
},
update: (value: string) => {
// верните `true` или `false` чтобы обозначить
// валидация пройдена / провалена
}
})
// основанный на типах
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>Аргумент для определения через тип может быть одним из следующих:
- Типами функций, но записанный как литерал типа с сигнатурой вызова. Он будет использоваться в качестве типа возвращаемой функции
emit. - Литеральный тип, где ключами являются имена событий, а значениями - типы массивов / кортежей, представляющие дополнительные принимаемые параметры для события. В приведенном примере используются именованные кортежи, поэтому каждый аргумент может иметь явное имя.
Как мы видим, объявление типа дает нам гораздо более тонкий контроль над ограничениями типов для пользовательских событий.
Если не используется <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 Error: Тип '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 // ок!Или, передав общий аргумент при вызове ref() переопределить вывод по умолчанию:
ts
// итоговый тип: Ref<string | number>
const year = ref<string | number>('2020')
year.value = 2020 // ок!Если указать аргумент типа generic, но опустить начальное значение, то результирующим типом будет тип union, включающий undefined:
ts
// предполагаемый тип: Ref<number | undefined>
const n = ref<number>()Типизация reactive()
reactive() также неявно выводит тип из своего аргумента:
ts
import { reactive } from 'vue'
// предполагаемый тип: { title: string }
const book = reactive({ title: 'Руководство по Vue 3' })Чтобы явно ввести свойство reactive, мы можем использовать интерфейсы:
ts
import { reactive } from 'vue'
interface Book {
title: string
year?: number
}
const book: Book = reactive({ title: 'Руководство по Vue 3' })Совет
Не рекомендуется использовать аргумент для дженерика reactive() поскольку возвращаемый тип, который обрабатывает разворачивание вложенных ссылок, отличается от типа аргумента дженерика.
Типизация computed()
computed() определяет его тип на основе возвращаемого значения геттера:
ts
import { ref, computed } from 'vue'
const count = ref(0)
// предполагаемый тип: ComputedRef<number>
const double = computed(() => count.value * 2)
// => TS Error: Свойство '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. Это также приведет к ошибке, если в tsconfig.json используются значения "strict": true или "noImplicitAny": true. Поэтому рекомендуется явно аннотировать аргумент обработчиков событий. Кроме того, может потребоваться явное приведение свойств к event:
ts
function handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value)
}Типизация Provide / Inject
Provide и inject обычно выполняются в отдельных компонентах. Для правильной типизации внедряемых значений Vue предоставляет интерфейс InjectionKey, который представляет собой общий тип, расширяющий Symbol. Он может быть использован для синхронизации типа внедряемого значения между провайдером и потребителем:
ts
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'
const key = Symbol() as InjectionKey<string>
provide(key, 'foo') // предоставление нестрокового значения приведет к ошибке
const foo = inject(key) // тип foo: string | undefinedРекомендуется помещать ключ инъекции в отдельный файл, чтобы его можно было импортировать в несколько компонентов.
При использовании строковых ключей инъекции тип инжектируемого значения будет unknown и должен быть явно объявлен через аргумент generic type:
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Типизация ссылок на шаблоны
В Vue 3.5 и @vue/language-tools 2.1 (используется в языковой службе IDE и в vue-tsc) тип ссылок, создаваемых useTemplateRef() в SFC, может автоматически выводиться для статических ref по тому, на какой элемент указывает атрибут ref.
Если автоматический вывод невозможен, тип ссылки на шаблон можно задать явно через аргумент дженерика:
ts
const el = useTemplateRef<HTMLInputElement>('el')Использование до версии 3.5
Шаблонные ссылки должны создаваться с явным аргументом типа generic и начальным значением 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 необходимо использовать опциональную цепочку или защиту типов. Это связано с тем, что начальное значение ref является null до тех пор, пока компонент не будет монтирован, а также может быть установлено в null, если ссылаемый элемент будет размонтирован с помощью v-if.
Типизация ссылок на шаблоны компонентов
В Vue 3.5 и @vue/language-tools 2.1 (используется в языковой службе IDE и в vue-tsc) тип ссылок, создаваемых useTemplateRef() в SFC, может автоматически выводиться для статических ref по тому, на какой элемент или компонент указывает атрибут ref.
Если автоматический вывод невозможен (например, использование вне SFC или динамические компоненты), тип ссылки на шаблон можно задать явно через аргумент дженерика.
Чтобы получить тип экземпляра импортированного компонента, нужно сначала получить его тип через typeof, затем воспользоваться встроенной утилитой TypeScript InstanceType:
vue
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
type FooType = InstanceType<typeof Foo>
type BarType = InstanceType<typeof Bar>
const compRef = useTemplateRef<FooType | BarType>('comp')
</script>
<template>
<component :is="Math.random() > 0.5 ? Foo : Bar" ref="comp" />
</template>В случаях, когда точный тип компонента недоступен или не важен, вместо него можно использовать ComponentPublicInstance. В этом случае будут указаны только те свойства, которые являются общими для всех компонентов, например $el:
ts
import { useTemplateRef } from 'vue'
import type { ComponentPublicInstance } from 'vue'
const child = useTemplateRef<ComponentPublicInstance>('child')В случаях, когда компонент, на который делается ссылка (ref), является дженерик-компонентом, например MyGenericModal:
vue
<script setup lang="ts" generic="ContentType extends string | number">
import { ref } from 'vue'
const content = ref<ContentType | null>(null)
const open = (newContent: ContentType) => (content.value = newContent)
defineExpose({
open
})
</script>На него необходимо ссылаться с помощью ComponentExposed из библиотеки vue-component-type-helpers, поскольку InstanceType не будет работать.
vue
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import MyGenericModal from './MyGenericModal.vue'
import type { ComponentExposed } from 'vue-component-type-helpers'
const modal =
useTemplateRef<ComponentExposed<typeof MyGenericModal>>('modal')
const openModal = () => {
modal.value?.open('newValue')
}
</script>Обратите внимание: в @vue/language-tools 2.1+ типы статических ссылок на шаблон выводятся автоматически, описанный выше подход нужен только в особых случаях.
Типизация глобальных пользовательских директив
Чтобы получить подсказки типов и проверку типов для глобальных пользовательских директив, объявленных через app.directive(), можно расширить интерфейс ComponentCustomProperties
ts
import type { Directive } from 'vue'
export type HighlightDirective = Directive<HTMLElement, string>
declare module 'vue' {
export interface ComponentCustomProperties {
// prefix with v (v-highlight)
vHighlight: HighlightDirective
}
}
export default {
mounted: (el, binding) => {
el.style.backgroundColor = binding.value
}
} satisfies HighlightDirectivets
import highlight from './directives/highlight'
// ...other code
const app = createApp(App)
app.directive('highlight', highlight)Использование в компоненте:
vue
<template>
<p v-highlight="'blue'">This sentence is important!</p>
</template>