Por qué TypeScript con Vue
TypeScript aporta seguridad de tipos al desarrollo con Vue, lo que se traduce en beneficios concretos para cualquier proyecto:
- Detección temprana de errores: El compilador detecta errores antes de ejecutar el código, como pasar un
stringdonde se espera unnumbero acceder a una propiedad inexistente. - Autocompletado inteligente: Los editores como VS Code ofrecen sugerencias precisas basadas en los tipos definidos, acelerando el desarrollo.
- Refactorización segura: Renombrar una propiedad o cambiar la firma de una función actualiza automáticamente todas las referencias.
- Documentación implícita: Los tipos actúan como documentación viva del código, facilitando la comprensión de APIs y estructuras de datos.
Vue está escrito internamente en TypeScript y proporciona tipos para toda su API pública, lo que garantiza una integración nativa y sin fricciones.
Creación del proyecto con TypeScript
La forma recomendada de crear un proyecto Vue con TypeScript es mediante create-vue:
npm create vue@latest
Durante el asistente interactivo, se selecciona la opción Add TypeScript? con Yes. Esto genera un proyecto preconfigurado con:
- Archivos
.vuecon<script setup lang="ts"> - Configuración de TypeScript optimizada para Vue
- Extensión
.tspara archivos de lógica pura
La estructura generada incluye:
mi-proyecto/
├── src/
│ ├── App.vue
│ ├── main.ts
│ ├── components/
│ └── views/
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── env.d.ts
└── vite.config.ts
Configuración de tsconfig
Un proyecto Vue con TypeScript utiliza una estructura de configuración con múltiples archivos para separar responsabilidades.
tsconfig.json (raíz)
El archivo raíz actúa como referencia a las configuraciones específicas:
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
tsconfig.app.json
Configura el código de la aplicación Vue:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
]
}
Opciones clave:
"strict": trueactiva todas las comprobaciones estrictas de TypeScript, lo cual es altamente recomendado."moduleResolution": "bundler"habilita la resolución de módulos optimizada para Vite."jsx": "preserve"permite usar JSX/TSX si se necesita, delegando la transformación a Vite.- La sección
pathsconfigura el alias@para importaciones absolutas desdesrc/.
tsconfig.node.json
Configura los archivos de herramientas de build como vite.config.ts:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true
},
"include": ["vite.config.ts"]
}
env.d.ts
Este archivo declara tipos globales necesarios para que TypeScript reconozca los módulos .vue y las variables de entorno de Vite:
/// <reference types="vite/client" />
Esta declaración permite importar archivos .vue en archivos .ts sin errores de tipo.
vue-tsc para verificación de tipos
vue-tsc es una herramienta que extiende el compilador de TypeScript (tsc) para entender archivos .vue. Es esencial porque tsc estándar no puede procesar Single File Components.
Se ejecuta para verificar tipos en todo el proyecto:
npx vue-tsc --noEmit
El flag --noEmit indica que solo se verifican tipos sin generar archivos de salida. Este comando se incluye normalmente en el script de build:
{
"scripts": {
"build": "vue-tsc --noEmit && vite build",
"type-check": "vue-tsc --noEmit"
}
}
vue-tsc verifica tanto el código TypeScript dentro de <script setup lang="ts"> como las expresiones del template, detectando errores como props incorrectas o variables no definidas en la plantilla.
Tipos básicos en Vue con TypeScript
En <script setup lang="ts">, Vue infiere automáticamente los tipos en muchos casos. Sin embargo, es importante conocer cómo anotar tipos explícitamente.
Variables reactivas con tipos inferidos
Vue infiere el tipo de ref a partir del valor inicial:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0) // Ref<number>
const name = ref('Vue') // Ref<string>
const active = ref(true) // Ref<boolean>
const items = ref([1, 2, 3]) // Ref<number[]>
</script>
Anotación explícita de tipos
Cuando el tipo no se puede inferir del valor inicial o cuando se necesita un tipo más específico, se usa el genérico de ref:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref<number>(0)
const name = ref<string | null>(null)
const items = ref<string[]>([])
</script>
reactive con tipos
reactive infiere el tipo del objeto pasado. Para tipos complejos, se define una interfaz:
<script setup lang="ts">
import { reactive } from 'vue'
interface UserState {
name: string
age: number
email: string | null
}
const state = reactive<UserState>({
name: 'Ana',
age: 28,
email: null
})
</script>
computed con tipos
Las propiedades computadas infieren su tipo del valor retornado:
<script setup lang="ts">
import { ref, computed } from 'vue'
const price = ref(100)
const tax = ref(0.21)
// TypeScript infiere computed<number>
const total = computed(() => price.value * (1 + tax.value))
// Anotación explícita cuando es necesario
const formatted = computed<string>(() => `${total.value.toFixed(2)} €`)
</script>
Interfaces y type aliases
TypeScript permite definir tipos personalizados que se reutilizan en toda la aplicación. Esto es fundamental para mantener la consistencia de las estructuras de datos.
Definir interfaces
<script setup lang="ts">
import { ref } from 'vue'
interface Product {
id: number
name: string
price: number
category: string
inStock: boolean
}
interface CartItem {
product: Product
quantity: number
}
const products = ref<Product[]>([])
const cart = ref<CartItem[]>([])
</script>
Type aliases para uniones y utilidades
<script setup lang="ts">
type Status = 'pending' | 'active' | 'completed' | 'cancelled'
type ID = number | string
interface Task {
id: ID
title: string
status: Status
assignee?: string
}
</script>
Organizar tipos en archivos separados
Para proyectos grandes, los tipos se definen en archivos .ts independientes:
// src/types/product.ts
export interface Product {
id: number
name: string
price: number
description: string
inStock: boolean
}
export interface ProductFilter {
category?: string
minPrice?: number
maxPrice?: number
onlyInStock?: boolean
}
Se importan en los componentes:
<script setup lang="ts">
import { ref } from 'vue'
import type { Product, ProductFilter } from '@/types/product'
const products = ref<Product[]>([])
const filter = ref<ProductFilter>({})
</script>
La palabra clave type en la importación (import type) indica que solo se importa información de tipos, que se elimina en tiempo de compilación sin generar código JavaScript adicional.
Ejemplo práctico completo
A continuación, un componente completo que combina los conceptos anteriores:
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Todo {
id: number
text: string
completed: boolean
createdAt: Date
}
type FilterType = 'all' | 'active' | 'completed'
const todos = ref<Todo[]>([])
const newTodoText = ref('')
const filter = ref<FilterType>('all')
let nextId = 1
const filteredTodos = computed<Todo[]>(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed)
case 'completed':
return todos.value.filter(t => t.completed)
default:
return todos.value
}
})
const stats = computed(() => ({
total: todos.value.length,
active: todos.value.filter(t => !t.completed).length,
completed: todos.value.filter(t => t.completed).length
}))
function addTodo(): void {
if (newTodoText.value.trim() === '') return
todos.value.push({
id: nextId++,
text: newTodoText.value.trim(),
completed: false,
createdAt: new Date()
})
newTodoText.value = ''
}
function toggleTodo(id: number): void {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
function removeTodo(id: number): void {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>
<template>
<div class="todo-app">
<h1>Lista de tareas</h1>
<form @submit.prevent="addTodo">
<input v-model="newTodoText" placeholder="Nueva tarea..." />
<button type="submit">Añadir</button>
</form>
<div class="filters">
<button
v-for="f in (['all', 'active', 'completed'] as FilterType[])"
:key="f"
:class="{ active: filter === f }"
@click="filter = f"
>
{{ f }}
</button>
</div>
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
/>
<span :class="{ done: todo.completed }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">Eliminar</button>
</li>
</ul>
<p>Total: {{ stats.total }} | Activas: {{ stats.active }} | Completadas: {{ stats.completed }}</p>
</div>
</template>
Este ejemplo muestra la inferencia automática de tipos, anotaciones explícitas con genéricos, interfaces, type aliases y funciones tipadas, todo dentro de un componente Vue funcional con <script setup lang="ts">.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje