TypeScript con la Options API
Esta página supone que ya has leído las generalidades en la sección Usando Vue con TypeScript.
TIP
Aunque Vue soporta el uso de TypeScript con Options API, se recomienda usar Vue con TypeScript a través de Composition API ya que ofrece una inferencia de tipos más simple, eficiente y robusta.
Escritura de las Props de Componentes
La inferencia de tipos para las props en la Options API requiere el encapsulamiento del componente con defineComponent()
. Con ello, Vue es capaz de inferir los tipos para las props basándose en la opción props
, teniendo en cuenta opciones adicionales como required: true
y default
:
ts
import { defineComponent } from 'vue'
export default defineComponent({
// inferencia de tipo habilitada
props: {
name: String,
id: [Number, String],
msg: { type: String, required: true },
metadata: null
},
mounted() {
this.name // tipo: string | undefined
this.id // tipo: number | string | undefined
this.msg // tipo: string
this.metadata // tipo: any
}
})
Sin embargo, las opciones de las "props" en tiempo de ejecución sólo soportan el uso de funciones constructoras como tipo de una prop; no hay manera de especificar tipos complejos como objetos con propiedades anidadas o expresiones de llamada a funciones.
Para anotar tipos de props complejos, podemos utilizar el tipo de utilidad PropType
:
ts
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
interface Book {
title: string
author: string
year: number
}
export default defineComponent({
props: {
book: {
// proporcionar un tipo más específico a `Object`
type: Object as PropType<Book>,
required: true
},
// puede también anotar funciones
callback: Function as PropType<(id: number) => void>
},
mounted() {
this.book.title // string
this.book.year // number
// Error TS: el argumento de tipo 'string' no es
// asignable a un parámetro de tipo 'number'
this.callback?.('123')
}
})
Advertencias
Si tu versión de TypeScript es inferior a 4.7
, tienes que tener cuidado cuando utilices valores de función para las opciones de las props validator
y default
; asegúrate de utilizar funciones de flecha:
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>,
// Asegúrate de utilizar las funciones de flecha si
// tu versión de TypeScript es inferior a la 4.7
default: () => ({
title: 'Expresión de la Función Flecha'
}),
validator: (book: Book) => !!book.title
}
}
})
Esto evita que TypeScript tenga que inferir el tipo de this
dentro de estas funciones, lo que, desafortunadamente, puede hacer que la inferencia de tipo falle. Esta era una limitación de diseño previa, y ahora ha sido mejorada en TypeScript 4.7.
Escritura de Emits del Componente
Podemos declarar el tipo de payload esperado para un evento emitido usando la sintaxis de objeto de la opción emits
. Además, todos los eventos emitidos no declarados lanzarán un error de tipo cuando sean llamados:
ts
import { defineComponent } from 'vue'
export default defineComponent({
emits: {
addBook(payload: { bookName: string }) {
// realizar la validación en tiempo de ejecución
return payload.bookName.length > 0
}
},
methods: {
onSubmit() {
this.$emit('addBook', {
bookName: 123 // ¡Error de tipado!
})
this.$emit('non-declared-event') // ¡Error de tipado!
}
}
})
Escritura de Propiedades Computadas
Una propiedad computada infiere su tipo basándose en su valor de retorno:
ts
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return {
message: 'Hola!'
}
},
computed: {
greeting() {
return this.message + '!'
}
},
mounted() {
this.greeting // tipo: string
}
})
En algunos casos, es posible que quieras indicar explícitamente el tipo de una propiedad computada para asegurarte de que su implementación es correcta:
ts
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return {
message: 'Hola!'
}
},
computed: {
// indicar explícitamente el tipo de retorno
greeting(): string {
return this.message + '!'
},
// indicar una propiedad computada editable
greetingUppercased: {
get(): string {
return this.greeting.toUpperCase()
},
set(newValue: string) {
this.message = newValue.toUpperCase()
}
}
}
})
Las indicaciones explícitas también pueden ser necesarias en algunos casos extremos en los que TypeScript no puede inferir el tipo de una propiedad computada debido a bucles de inferencia circular.
Escritura de Manejadores de Eventos
Cuando se trata de eventos nativos del DOM, puede ser útil escribir correctamente el argumento que pasamos al manejador. Veamos este ejemplo:
vue
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
methods: {
handleChange(event) {
// `event` tiene implícitamente el tipo `any`.
console.log(event.target.value)
}
}
})
</script>
<template>
<input type="text" @change="handleChange" />
</template>
Sin anotación de tipo, el argumento event
tendrá implícitamente un tipo any
. Esto también dará lugar a un error de TS si se utiliza "strict": true
o "noImplicitAny": true
en tsconfig.json
. Por lo tanto, se recomienda indicar explícitamente el argumento de los manejadores de eventos. Además, es posible que tengas que usar aserciones de tipo al acceder las propiedades de event
:
ts
import { defineComponent } from 'vue'
export default defineComponent({
methods: {
handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value)
}
}
})
Aumento de las Propiedades Globales
Algunos plugins instalan propiedades disponibles globalmente a todas las instancias del componente a través de app.config.globalProperties
. Por ejemplo, podemos instalar this.$http
para la obtención de datos o this.$translate
para la internacionalización. Para que esto funcione bien con TypeScript, Vue expone una interfaz ComponentCustomProperties
diseñada para ser aumentada mediante TypeScript module augmentation:
ts
import axios from 'axios'
declare module 'vue' {
interface ComponentCustomProperties {
$http: typeof axios
$translate: (key: string) => string
}
}
Véase también:
Ubicación del Aumento de Tipo
Podemos poner este aumento de tipo en un archivo .ts
, o en un archivo de todo el proyecto *.d.ts
. De cualquier manera, asegúrate de que está incluido en tsconfig.json
. Para los autores de librerías / plugins, este archivo debe ser especificado en la propiedad types
en package.json
.
Para aprovechar las ventajas del aumento del módulo, tendrás que asegurarte de que el aumento se coloca en un módulo TypeScript. Es decir, el archivo necesita contener al menos un import
o export
de nivel superior, incluso si es sólo export {}
. Si el aumento se coloca fuera de un módulo, ¡sobreescribirá los tipos originales en lugar de aumentarlos!
ts
// No funciona, sobrescribe los tipos originales.
declare module 'vue' {
interface ComponentCustomProperties {
$translate: (key: string) => string
}
}
ts
// Funciona correctamente
export {}
declare module 'vue' {
interface ComponentCustomProperties {
$translate: (key: string) => string
}
}
Aumento de las Opciones Personalizadas
Algunos plugins, por ejemplo vue-router
, proporcionan soporte para opciones de componentes personalizados como beforeRouteEnter
:
ts
import { defineComponent } from 'vue'
export default defineComponent({
beforeRouteEnter(to, from, next) {
// ...
}
})
Sin un aumento de tipo adecuado, los argumentos de este hook tendrán implícitamente un tipo any
. Podemos aumentar la interfaz ComponentCustomOptions
para soportar estas opciones personalizadas:
ts
import { Route } from 'vue-router'
declare module 'vue' {
interface ComponentCustomOptions {
beforeRouteEnter?(to: Route, from: Route, next: () => void): void
}
}
Ahora la opción beforeRouteEnter
estará correctamente tipada. Ten en cuenta que esto es sólo un ejemplo; las librerías bien tipadas como vue-router
deberían realizar automáticamente estos aumentos en sus propias definiciones de tipo.
La colocación de este aumento está sujeta a las mismas restricciones que los aumentos de propiedades globales.
Véase también: