返回 Skills
vuejs-ai/skills· MIT 内容可用

vue-options-api-best-practices

Vue 3 Options API style (data(), methods, this context). Each reference shows Options API solution only.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: vue-options-api-best-practices description: "Vue 3 Options API style (data(), methods, this context). Each reference shows Options API solution only." version: 2.0.0 license: MIT author: github.com/vuejs-ai

Vue.js Options API best practices, TypeScript integration, and common gotchas.

TypeScript

Methods & Lifecycle

附带文件

reference/no-arrow-functions-in-lifecycle-hooks.md
---
title: Never Use Arrow Functions for Options API Lifecycle Hooks
impact: HIGH
impactDescription: Arrow functions in lifecycle hooks break `this` binding to component instance
type: capability
tags: [vue3, vue2, options-api, lifecycle, arrow-functions, this-binding, mounted, created]
---

# Never Use Arrow Functions for Options API Lifecycle Hooks

**Impact: HIGH** - Using arrow functions for lifecycle hooks in the Options API prevents Vue from binding `this` to the component instance. This causes `this` to be `undefined` or reference the wrong context, leading to runtime errors when accessing component data, methods, or other properties.

Arrow functions lexically bind `this` from their enclosing scope. Vue's Options API lifecycle hooks (created, mounted, updated, unmounted, etc.) require regular functions so Vue can set `this` to the component instance at runtime.

## Task Checklist

- [ ] Always use regular function syntax for Options API lifecycle hooks
- [ ] Use ES6 method shorthand (preferred) for cleaner code
- [ ] Arrow functions ARE allowed inside lifecycle hooks for callbacks

**Incorrect:**
```javascript
export default {
  data() {
    return { message: 'Hello' }
  },
  // WRONG: Arrow function - `this` will be undefined
  created: () => {
    console.log(this.message) // Error: Cannot read property 'message' of undefined
  },
  // WRONG: Arrow function for mounted
  mounted: () => {
    this.initializePlugin() // Error: this.initializePlugin is not a function
  },
  // WRONG: Arrow function for beforeUnmount
  beforeUnmount: () => {
    this.cleanup() // Will fail!
  },
  methods: {
    initializePlugin() { /* ... */ },
    cleanup() { /* ... */ }
  }
}
```

**Correct:**
```javascript
export default {
  data() {
    return { message: 'Hello' }
  },
  // CORRECT: ES6 method shorthand (preferred)
  created() {
    console.log(this.message) // Works! this refers to component instance
  },
  // CORRECT: Regular function expression
  mounted: function() {
    this.initializePlugin() // Works!
  },
  // CORRECT: Method shorthand
  beforeUnmount() {
    this.cleanup() // Works!
  },
  methods: {
    initializePlugin() {
      // Arrow functions ARE fine for callbacks inside lifecycle hooks
      this.$nextTick(() => {
        this.isReady = true // Arrow inherits `this` from mounted
      })
    },
    cleanup() { /* ... */ }
  }
}
```

## All Affected Lifecycle Hooks

The following Options API hooks must NOT use arrow functions:
- `beforeCreate`
- `created`
- `beforeMount`
- `mounted`
- `beforeUpdate`
- `updated`
- `beforeUnmount` (Vue 3) / `beforeDestroy` (Vue 2)
- `unmounted` (Vue 3) / `destroyed` (Vue 2)
- `activated`
- `deactivated`
- `errorCaptured`
- `renderTracked`
- `renderTriggered`

## Reference
- [Vue.js Lifecycle Hooks](https://vuejs.org/guide/essentials/lifecycle.html)
- [Vue.js Options Lifecycle](https://vuejs.org/api/options-lifecycle.html)
reference/no-arrow-functions-in-methods.md
---
title: Never Use Arrow Functions in Methods Option
impact: HIGH
impactDescription: Arrow functions prevent Vue from binding `this` to the component instance
type: capability
tags: [vue3, vue2, options-api, methods, this-binding]
---

# Never Use Arrow Functions in Methods Option

**Impact: HIGH** - Using arrow functions in the `methods` option causes `this` to be `undefined` or the wrong context, leading to runtime errors when trying to access component data, computed properties, or other methods.

Arrow functions lexically bind `this` from their enclosing scope, not from the object they're defined on. Vue's `methods` option requires regular functions so Vue can bind `this` to the component instance.

## Task Checklist

- [ ] Always use regular function syntax for methods in Options API
- [ ] If using ES6 shorthand, use method shorthand (preferred)
- [ ] Arrow functions ARE allowed inside methods for callbacks

**Incorrect:**
```javascript
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    // WRONG: Arrow function - `this` will be undefined
    increment: () => {
      this.count++ // Error: Cannot read property 'count' of undefined
    },
    // WRONG: Arrow function assigned to property
    decrement: () => {
      this.count--
    }
  }
}
```

**Correct:**
```javascript
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    // CORRECT: ES6 method shorthand (preferred)
    increment() {
      this.count++ // Works! this refers to component instance
    },
    // CORRECT: Traditional function expression
    decrement: function() {
      this.count--
    },
    // Arrow functions ARE fine for callbacks INSIDE methods
    fetchData() {
      fetch('/api/data')
        .then(response => response.json())
        .then(data => {
          this.data = data // Arrow function inherits `this` from fetchData
        })
    }
  }
}
```

## Reference
- [Vue.js Methods - Avoid Arrow Functions](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#methods)
reference/stateful-methods-lifecycle.md
---
title: Create Stateful Methods in Lifecycle Hooks
impact: MEDIUM
impactDescription: Stateful functions like debounce/throttle in methods are shared across all component instances
type: capability
tags: [vue3, options-api, debounce, throttle, lifecycle, cleanup]
---

# Create Stateful Methods in Lifecycle Hooks

**Impact: MEDIUM** - If you define debounced, throttled, or other stateful functions directly in the `methods` option, all instances of the component share the same function state. This causes race conditions and bugs in lists of components.

When a component is reused (e.g., in v-for), each instance needs its own debounced/throttled function. Define these in the `created()` hook and clean them up in `unmounted()` to prevent memory leaks.

## Task Checklist

- [ ] Never define debounced/throttled functions directly in `methods`
- [ ] Create stateful functions in `created()` lifecycle hook
- [ ] Always clean up (cancel timers) in `unmounted()`

**Incorrect:**
```javascript
import { debounce } from 'lodash-es'

export default {
  methods: {
    // WRONG: All component instances share this debounced function!
    // If used in a v-for, clicking one button affects all instances
    handleClick: debounce(function() {
      this.performSearch()
    }, 500)
  }
}
```

**Correct:**
```javascript
import { debounce } from 'lodash-es'

export default {
  created() {
    // CORRECT: Each instance gets its own debounced function
    this.debouncedSearch = debounce(this.performSearch, 500)
  },
  unmounted() {
    // CORRECT: Clean up to prevent memory leaks and stale calls
    this.debouncedSearch.cancel()
  },
  methods: {
    handleClick() {
      this.debouncedSearch()
    },
    performSearch() {
      // Actual search logic
    }
  }
}
```

## Reference
- [Vue.js Reactivity Fundamentals - Stateful Methods](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#stateful-methods)
reference/ts-options-api-arrow-functions-validators.md
---
title: Use Arrow Functions for Prop Validators in TypeScript < 4.7
impact: HIGH
impactDescription: Regular functions in prop validators can break type inference for the entire component in TypeScript versions before 4.7
type: gotcha
tags: [vue3, typescript, options-api, props, type-inference, defineComponent, legacy]
---

# Use Arrow Functions for Prop Validators in TypeScript < 4.7

> **LEGACY CONCERN:** This issue was fixed in TypeScript 4.7 (released May 2022). Most modern projects using TypeScript 4.7+ do not need this workaround. If you're starting a new project or have upgraded TypeScript recently, you can safely use regular functions in prop validators. This rule is primarily relevant for legacy codebases still running TypeScript < 4.7.

**Impact: HIGH** - If your TypeScript version is less than 4.7, using regular functions for `validator` and `default` prop options can cause TypeScript to fail when inferring the type of `this`, which breaks type inference for the ENTIRE component, not just the prop.

## Task Checklist

- [ ] Check your TypeScript version (`tsc --version`)
- [ ] If TypeScript < 4.7, use arrow functions for all `validator` and `default` prop options
- [ ] Consider upgrading to TypeScript 4.7+ to avoid this limitation entirely

## The Problem

TypeScript needs to infer the type of `this` inside regular functions. In Vue's Options API context, this inference can fail in versions before 4.7, causing cascading type inference failures.

**BAD - Can break type inference in TS < 4.7:**
```typescript
import { defineComponent, PropType } from 'vue'

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

export default defineComponent({
  props: {
    book: {
      type: Object as PropType<Book>,
      required: true,
      // Regular function - causes inference issues in TS < 4.7
      validator: function(book: Book) {
        return book.title.length > 0
      }
    },
    count: {
      type: Number,
      // Regular function - causes inference issues in TS < 4.7
      default: function() {
        return 0
      }
    }
  },
  // Type inference for computed, methods, etc. may break!
  computed: {
    bookTitle() {
      // 'this' might be typed as 'any' due to broken inference
      return this.book.title
    }
  }
})
```

**GOOD - Use arrow functions:**
```typescript
import { defineComponent, PropType } from 'vue'

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

export default defineComponent({
  props: {
    book: {
      type: Object as PropType<Book>,
      required: true,
      // Arrow function - safe for all TS versions
      validator: (book: Book) => book.title.length > 0
    },
    count: {
      type: Number,
      // Arrow function - safe for all TS versions
      default: () => 0
    }
  },
  computed: {
    bookTitle() {
      // 'this' is properly typed
      return this.book.title  // Type: string
    }
  }
})
```

## Why Arrow Functions Work

Arrow functions don't have their own `this` binding, so TypeScript doesn't need to infer a `this` type for them. This avoids the type inference bug that affects regular functions in older TypeScript versions.

## For Object/Array Default Values

Arrow functions are especially important for object and array defaults (which Vue requires to be functions anyway):

```typescript
props: {
  config: {
    type: Object as PropType<Config>,
    // Must be a function, use arrow syntax
    default: () => ({
      enabled: true,
      maxItems: 10
    })
  },
  tags: {
    type: Array as PropType<string[]>,
    // Must be a function, use arrow syntax
    default: () => []
  }
}
```

## When You Can Ignore This

- **TypeScript 4.7+**: This issue was fixed in TypeScript 4.7 (May 2022). If your project uses 4.7 or later, regular functions work fine. Since TypeScript 4.7 has been available for over 3 years, most actively maintained projects have already upgraded and do not need this workaround.
- **New projects**: If you're starting a new Vue project in 2024 or later, you'll almost certainly be using TypeScript 4.7+ by default and can ignore this rule entirely.
- **Arrow functions are still fine**: While not required in modern TypeScript, using arrow functions for validators and defaults remains a valid stylistic choice and causes no issues.

## Checking Your TypeScript Version

```bash
# Check installed TypeScript version
npx tsc --version

# Or check package.json
grep typescript package.json
```

## Reference

- [Vue.js TypeScript with Options API](https://vuejs.org/guide/typescript/options-api.html#caveats)
- [TypeScript 4.7 Release Notes](https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/)
reference/ts-options-api-computed-return-types.md
---
title: Explicitly Annotate Computed Property Return Types
impact: LOW
impactDescription: While Vue usually infers computed types correctly, explicit annotations prevent circular inference issues and improve code documentation
type: best-practice
tags: [vue3, typescript, options-api, computed, type-inference]
---

# Explicitly Annotate Computed Property Return Types

**Impact: LOW** - Vue can usually infer computed property return types automatically. However, explicit return type annotations prevent edge cases involving circular inference loops and serve as documentation for complex computed properties.

## Task Checklist

- [ ] Consider adding return types to computed properties, especially complex ones
- [ ] Always add return types when TypeScript inference fails or shows incorrect types
- [ ] Add return types for writable computed properties (getter and setter)

## When Explicit Types Are Helpful

### 1. Complex Computed Properties

```typescript
import { defineComponent } from 'vue'

interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
}

export default defineComponent({
  data() {
    return {
      items: [] as CartItem[],
      discountPercent: 10
    }
  },
  computed: {
    // Without annotation - works but intent unclear
    cartSummary() {
      return {
        subtotal: this.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
        itemCount: this.items.reduce((sum, item) => sum + item.quantity, 0)
      }
    },

    // With annotation - clear intent, documented type
    cartSummaryTyped(): { subtotal: number; itemCount: number; discount: number; total: number } {
      const subtotal = this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
      const discount = subtotal * (this.discountPercent / 100)
      return {
        subtotal,
        itemCount: this.items.reduce((sum, item) => sum + item.quantity, 0),
        discount,
        total: subtotal - discount
      }
    }
  }
})
```

### 2. Circular Inference Issues

Sometimes computed properties that reference each other can cause TypeScript inference to fail:

```typescript
export default defineComponent({
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    // Explicit return type breaks potential circular inference
    fullName(): string {
      return `${this.firstName} ${this.lastName}`
    },

    // References fullName - explicit type prevents inference issues
    greeting(): string {
      return `Hello, ${this.fullName}!`
    }
  }
})
```

### 3. Writable Computed Properties

Always annotate writable computed properties for clarity:

```typescript
export default defineComponent({
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    // Writable computed - explicit types for getter and setter
    fullName: {
      get(): string {
        return `${this.firstName} ${this.lastName}`
      },
      set(newValue: string) {
        const parts = newValue.split(' ')
        this.firstName = parts[0] || ''
        this.lastName = parts.slice(1).join(' ') || ''
      }
    }
  }
})
```

## When You Can Skip Explicit Types

Simple computed properties that TypeScript infers correctly:

```typescript
computed: {
  // Simple string - TypeScript infers correctly
  upperName() {
    return this.name.toUpperCase()
  },

  // Simple boolean - TypeScript infers correctly
  isEmpty() {
    return this.items.length === 0
  },

  // Simple number - TypeScript infers correctly
  totalCount() {
    return this.items.length
  }
}
```

## Interface for Complex Return Types

For complex computed return types, define interfaces:

```typescript
interface PaginationInfo {
  currentPage: number
  totalPages: number
  hasNext: boolean
  hasPrev: boolean
  pageItems: Item[]
}

export default defineComponent({
  computed: {
    pagination(): PaginationInfo {
      const totalPages = Math.ceil(this.items.length / this.pageSize)
      const start = (this.currentPage - 1) * this.pageSize

      return {
        currentPage: this.currentPage,
        totalPages,
        hasNext: this.currentPage < totalPages,
        hasPrev: this.currentPage > 1,
        pageItems: this.items.slice(start, start + this.pageSize)
      }
    }
  }
})
```

## Debugging Type Inference

If you're unsure what TypeScript infers, hover over the computed property in your IDE, or add a temporary annotation to see errors:

```typescript
computed: {
  // Hover over 'mystery' in IDE to see inferred type
  mystery() {
    return this.someComplexCalculation()
  },

  // Or add annotation to verify
  mystery(): ExpectedType {  // Error if inference differs
    return this.someComplexCalculation()
  }
}
```

## Reference

- [Vue.js TypeScript with Options API - Typing Computed Properties](https://vuejs.org/guide/typescript/options-api.html#typing-computed-properties)
reference/ts-options-api-proptype-complex-types.md
---
title: Use PropType for Complex Prop Types in Options API
impact: MEDIUM
impactDescription: Vue's runtime prop types cannot express complex TypeScript types without PropType, leading to 'any' type inference
type: best-practice
tags: [vue3, typescript, options-api, props, PropType, type-safety]
---

# Use PropType for Complex Prop Types in Options API

**Impact: MEDIUM** - Vue's runtime `props` option only supports basic constructor functions (String, Number, etc.). To type complex props like interfaces, function signatures, or union types, you must use Vue's `PropType` utility type.

## Task Checklist

- [ ] Import `PropType` from 'vue' for complex prop types
- [ ] Use `as PropType<YourType>` after `Object`, `Array`, or `Function`
- [ ] Define interfaces for your complex types
- [ ] Remember: PropType is purely for TypeScript - runtime validation only checks the constructor

## The Problem

Vue's runtime prop system uses JavaScript constructor functions, which can't express complex TypeScript types:

```typescript
// What runtime props support:
props: {
  name: String,       // OK: string
  count: Number,      // OK: number
  enabled: Boolean,   // OK: boolean
  items: Array,       // Problem: any[]
  config: Object,     // Problem: Record<string, any>
  handler: Function   // Problem: (...args: any[]) => any
}
```

## Using PropType for Complex Types

**Import and use PropType:**
```typescript
import { defineComponent, PropType } from 'vue'
// or
import type { PropType } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

interface Config {
  theme: 'light' | 'dark'
  maxItems: number
}

export default defineComponent({
  props: {
    // Object with interface
    user: {
      type: Object as PropType<User>,
      required: true
    },

    // Array of typed items
    users: {
      type: Array as PropType<User[]>,
      default: () => []
    },

    // Object with union type
    config: {
      type: Object as PropType<Config>,
      default: () => ({ theme: 'light', maxItems: 10 })
    },

    // Typed function
    onSubmit: {
      type: Function as PropType<(data: User) => Promise<void>>,
      required: true
    },

    // Union of primitives
    id: {
      type: [String, Number] as PropType<string | number>,
      required: true
    },

    // Literal union type
    status: {
      type: String as PropType<'pending' | 'active' | 'completed'>,
      default: 'pending'
    }
  },

  methods: {
    async handleSubmit() {
      // Full type inference!
      await this.onSubmit(this.user)  // onSubmit is properly typed
      console.log(this.user.email)    // user.email is string
      console.log(this.config.theme)  // theme is 'light' | 'dark'
    }
  }
})
```

## Important: Runtime vs Compile-Time

PropType only affects TypeScript compilation. At runtime, Vue still only validates using the constructor:

```typescript
props: {
  user: {
    type: Object as PropType<User>,
    required: true
  }
}

// Runtime: Vue only checks typeof value === 'object'
// It does NOT validate { id, name, email } structure

// To add runtime validation, use validator:
props: {
  user: {
    type: Object as PropType<User>,
    required: true,
    validator: (user: User) => {
      return typeof user.id === 'number' &&
             typeof user.name === 'string' &&
             typeof user.email === 'string'
    }
  }
}
```

## Common PropType Patterns

### Nullable Props

```typescript
props: {
  // Optional object that can be null
  selectedItem: {
    type: Object as PropType<Item | null>,
    default: null
  }
}
```

### Enum-like Props

```typescript
type ButtonVariant = 'primary' | 'secondary' | 'danger'

props: {
  variant: {
    type: String as PropType<ButtonVariant>,
    default: 'primary',
    validator: (v: ButtonVariant) =>
      ['primary', 'secondary', 'danger'].includes(v)
  }
}
```

### Generic-like Props with Multiple Types

```typescript
props: {
  // Accept string, number, or object with id
  value: {
    type: [String, Number, Object] as PropType<string | number | { id: string }>,
    required: true
  }
}
```

### Event Handler Props

```typescript
interface ClickEventData {
  item: Item
  index: number
}

props: {
  onClick: {
    type: Function as PropType<(data: ClickEventData) => void>,
    required: false
  }
}
```

## Why Not Just Use `as`?

You might think to skip `PropType`:

```typescript
// WRONG - doesn't work as expected
props: {
  user: Object as User  // TypeScript error or incorrect inference
}

// CORRECT - use PropType
props: {
  user: Object as PropType<User>
}
```

`PropType<T>` is specifically designed to work with Vue's prop type system.

## Reference

- [Vue.js TypeScript with Options API - Typing Component Props](https://vuejs.org/guide/typescript/options-api.html#typing-component-props)
- [Vue.js Props Documentation](https://vuejs.org/guide/components/props.html)
reference/ts-options-api-provide-inject-limitations.md
---
title: Provide/Inject Has Limited Type Inference in Options API
impact: MEDIUM
impactDescription: Injected properties in Options API are not automatically typed, requiring manual type assertions or type augmentation
type: gotcha
tags: [vue3, typescript, options-api, provide-inject, type-safety]
---

# Provide/Inject Has Limited Type Inference in Options API

**Impact: MEDIUM** - When using `provide/inject` with the Options API and TypeScript, injected properties won't be automatically typed. TypeScript will show errors that the property doesn't exist on the component, even though it works at runtime.

## Task Checklist

- [ ] Be aware that Options API inject has limited type support
- [ ] Use type augmentation to add types to injected values
- [ ] Consider using typed computed wrappers for injected values

## The Problem

```typescript
// Provider component (parent)
import { defineComponent } from 'vue'

export default defineComponent({
  provide() {
    return {
      theme: 'dark',
      user: { id: 1, name: 'John' }
    }
  }
})
```

```typescript
// Consumer component (child) - Options API
import { defineComponent } from 'vue'

export default defineComponent({
  inject: ['theme', 'user'],

  mounted() {
    // TypeScript Error: Property 'theme' does not exist on type...
    console.log(this.theme)

    // TypeScript Error: Property 'user' does not exist on type...
    console.log(this.user.name)
  }
})
```

## Solution 1: Type Augmentation

Augment the component type to include injected properties:

```typescript
// types/injections.d.ts
import 'vue'

declare module 'vue' {
  interface ComponentCustomProperties {
    theme: 'light' | 'dark'
    user: { id: number; name: string }
  }
}

export {}
```

```typescript
// Consumer component - now typed
import { defineComponent } from 'vue'

export default defineComponent({
  inject: ['theme', 'user'],

  mounted() {
    // Now works - typed from ComponentCustomProperties
    console.log(this.theme)  // 'light' | 'dark'
    console.log(this.user.name)  // string
  }
})
```

**Note**: This adds types globally to ALL components, not just those that inject these values.

## Solution 2: Typed Computed Wrappers

Use the object syntax with computed property wrappers for type safety:

```typescript
import { defineComponent } from 'vue'

interface User {
  id: number
  name: string
}

export default defineComponent({
  inject: {
    theme: {
      from: 'theme',
      default: 'light'
    },
    user: {
      from: 'user',
      default: () => ({ id: 0, name: 'Guest' })
    }
  },

  computed: {
    // Type via computed wrapper
    typedTheme(): 'light' | 'dark' {
      return this.theme as 'light' | 'dark'
    },
    typedUser(): User {
      return this.user as User
    }
  },

  mounted() {
    console.log(this.typedTheme)
    console.log(this.typedUser.name)
  }
})
```

## Why This Happens

The Options API `inject` array syntax `inject: ['theme']` doesn't provide type information to TypeScript. Vue knows about the injection at runtime, but TypeScript's static analysis can't trace the provide/inject relationship across components.

## Reference

- [Vue.js Provide/Inject](https://vuejs.org/guide/components/provide-inject.html)
- [GitHub Issue: Add type inference to Options API provide/inject](https://github.com/vuejs/core/issues/3031)
reference/ts-options-api-type-event-handlers.md
---
title: Explicitly Type Event Handlers in Options API Methods
impact: MEDIUM
impactDescription: Untyped event handler arguments default to 'any', missing type errors and losing IDE support for DOM event properties
type: best-practice
tags: [vue3, typescript, options-api, events, type-safety, DOM]
---

# Explicitly Type Event Handlers in Options API Methods

**Impact: MEDIUM** - Without explicit type annotations, event handler parameters in Options API methods are typed as `any`. This defeats TypeScript's purpose, causing `noImplicitAny` errors in strict mode and losing type safety for DOM event properties.

## Task Checklist

- [ ] Always add type annotations to event handler method parameters
- [ ] Use the correct DOM event type (Event, MouseEvent, KeyboardEvent, etc.)
- [ ] Use type assertions for event.target when accessing element-specific properties
- [ ] Enable `strict: true` in tsconfig.json to catch implicit any errors

## The Problem

```typescript
import { defineComponent } from 'vue'

export default defineComponent({
  methods: {
    // BAD - 'event' is implicitly 'any'
    handleClick(event) {
      console.log(event.target.value)  // No type checking!
    },

    // BAD - Causes error with noImplicitAny
    handleInput(event) {
      // Error: Parameter 'event' implicitly has an 'any' type
      this.searchTerm = event.target.value
    }
  }
})
```

## The Solution: Explicit Event Types

```typescript
import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return {
      searchTerm: ''
    }
  },
  methods: {
    // GOOD - Explicit Event type
    handleClick(event: MouseEvent) {
      console.log(event.clientX, event.clientY)
      // Cast target for element-specific properties
      const button = event.target as HTMLButtonElement
      console.log(button.disabled)
    },

    // GOOD - Explicit Event type with target assertion
    handleInput(event: Event) {
      const input = event.target as HTMLInputElement
      this.searchTerm = input.value
    },

    // GOOD - KeyboardEvent for keyboard handlers
    handleKeydown(event: KeyboardEvent) {
      if (event.key === 'Enter') {
        this.submit()
      }
    }
  }
})
```

## Common DOM Event Types

| Event Type | Use For |
|------------|---------|
| `Event` | Generic events, custom events |
| `MouseEvent` | click, dblclick, mouseenter, mouseleave, etc. |
| `KeyboardEvent` | keydown, keyup, keypress |
| `InputEvent` | input (modern browsers) |
| `FocusEvent` | focus, blur |
| `SubmitEvent` | form submit |
| `DragEvent` | drag, drop, dragenter, dragover |
| `TouchEvent` | touchstart, touchend, touchmove |
| `WheelEvent` | wheel |

## Type Assertions for event.target

`event.target` is typed as `EventTarget | null`, which is too generic. Use type assertions:

```typescript
methods: {
  // Input element
  onInputChange(event: Event) {
    const target = event.target as HTMLInputElement
    console.log(target.value)
    console.log(target.checked)  // for checkboxes
  },

  // Select element
  onSelectChange(event: Event) {
    const target = event.target as HTMLSelectElement
    console.log(target.value)
    console.log(target.selectedIndex)
  },

  // Form element
  onFormSubmit(event: SubmitEvent) {
    event.preventDefault()
    const form = event.target as HTMLFormElement
    const formData = new FormData(form)
  },

  // Using currentTarget (often more reliable)
  onButtonClick(event: MouseEvent) {
    // currentTarget is the element the handler is attached to
    const button = event.currentTarget as HTMLButtonElement
    console.log(button.dataset.id)
  }
}
```

## Template Usage

```vue
<template>
  <div>
    <!-- Mouse events -->
    <button @click="handleClick">Click me</button>

    <!-- Input events -->
    <input @input="handleInput" @keydown="handleKeydown" />

    <!-- Form events -->
    <form @submit.prevent="handleSubmit">
      <button type="submit">Submit</button>
    </form>
  </div>
</template>
```

## With Custom Component Events

For custom component events, type based on what the component emits:

```typescript
methods: {
  // Child emits: emit('update', { id: number, value: string })
  handleChildUpdate(payload: { id: number; value: string }) {
    console.log(payload.id, payload.value)
  },

  // Child emits primitive: emit('change', newValue)
  handleValueChange(newValue: number) {
    this.count = newValue
  }
}
```

## Generic Event Handler Pattern

For reusable handlers:

```typescript
methods: {
  // Generic handler that works with any input
  updateField<T extends HTMLInputElement | HTMLTextAreaElement>(
    field: keyof typeof this.$data,
    event: Event
  ) {
    const target = event.target as T
    ;(this as any)[field] = target.value
  }
}
```

## Why currentTarget vs target?

- **target**: The element that triggered the event (could be a child)
- **currentTarget**: The element the event listener is attached to

```typescript
methods: {
  // If button contains <span>Click</span>, clicking the span:
  // - event.target = span
  // - event.currentTarget = button
  handleClick(event: MouseEvent) {
    // Use currentTarget when you want the handler's element
    const button = event.currentTarget as HTMLButtonElement
  }
}
```

## Reference

- [Vue.js TypeScript with Options API - Typing Event Handlers](https://vuejs.org/guide/typescript/options-api.html#typing-event-handlers)
- [MDN Event Interface](https://developer.mozilla.org/en-US/docs/Web/API/Event)
- [TypeScript DOM Types](https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts)
reference/ts-options-api-use-definecomponent.md
---
title: Always Use defineComponent for TypeScript Type Inference
impact: HIGH
impactDescription: Without defineComponent, TypeScript cannot infer types for props, computed properties, methods, or the 'this' context in Options API components
type: best-practice
tags: [vue3, typescript, options-api, defineComponent, type-inference]
---

# Always Use defineComponent for TypeScript Type Inference

**Impact: HIGH** - When using TypeScript with Vue's Options API, you MUST wrap your component definition with `defineComponent()` to enable proper type inference. Without it, `this` is typed as `any`, losing all TypeScript benefits.

## Task Checklist

- [ ] Always import and use `defineComponent` from 'vue' for Options API components
- [ ] Enable `strict: true` (or at minimum `noImplicitThis: true`) in tsconfig.json
- [ ] Consider migrating to Composition API with `<script setup>` for better type inference

## The Problem

Vue's Options API relies heavily on the `this` context, which TypeScript cannot automatically type without `defineComponent`:

**BAD - No type inference:**
```typescript
// No defineComponent - 'this' is typed as 'any'
export default {
  props: {
    message: String
  },
  computed: {
    // 'this' is 'any' - no type checking!
    greeting() {
      return this.message + '!'  // No type inference
    }
  },
  methods: {
    // 'this' is 'any' - mistakes won't be caught
    handleClick() {
      console.log(this.mesage)  // Typo not caught!
    }
  }
}
```

**GOOD - Full type inference:**
```typescript
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    message: {
      type: String,
      required: true
    },
    count: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      localState: ''
    }
  },
  computed: {
    // 'this.message' is typed as string
    // 'this.count' is typed as number
    greeting(): string {
      return this.message + '!'
    }
  },
  methods: {
    handleClick() {
      console.log(this.mesage)  // Error: Property 'mesage' does not exist
      console.log(this.message)  // OK: string
    }
  }
})
```

## What defineComponent Enables

1. **Props type inference**: Vue infers types from `type`, `required`, and `default`
2. **`this` context typing**: All options (data, computed, methods) are properly typed
3. **Cross-option references**: Access data in methods, computed properties, etc. with full types
4. **IDE autocompletion**: Get suggestions for all component properties and methods

## TypeScript Configuration Required

For proper `this` type checking, enable strict mode or at minimum `noImplicitThis`:

```json
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    // Or at minimum:
    "noImplicitThis": true
  }
}
```

Without this, TypeScript allows implicit `any` for `this`, defeating the purpose of using `defineComponent`.

## defineComponent is a No-Op at Runtime

`defineComponent` does nothing at runtime - it simply returns the object you pass to it. Its only purpose is to help TypeScript infer types:

```typescript
// At runtime, this is equivalent to:
// export default { props: { ... }, ... }
export default defineComponent({
  props: { /* ... */ }
})
```

This means there's zero runtime cost to using `defineComponent`.

## When to Use defineComponent vs script setup

| Approach | Use Case |
|----------|----------|
| `defineComponent` | Options API, Class-based migration, JSX/TSX components |
| `<script setup>` | New components, better type inference, less boilerplate |

**Official recommendation**: "While Vue does support TypeScript usage with Options API, it is recommended to use Vue with TypeScript via Composition API as it offers simpler, more efficient and more robust type inference."

## With Vue Single-File Components

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

export default defineComponent({
  name: 'MyComponent',
  props: {
    title: {
      type: String,
      required: true
    }
  },
  computed: {
    upperTitle(): string {
      return this.title.toUpperCase()
    }
  }
})
</script>

<template>
  <h1>{{ upperTitle }}</h1>
</template>
```

## Common Mistake: Missing defineComponent

This often happens when copying JavaScript components to TypeScript:

```typescript
// Copied from JS - MISSING defineComponent!
export default {
  name: 'MyComponent',
  // ... entire component without type inference
}
```

Always add `defineComponent` when converting to TypeScript.

## Reference

- [Vue.js TypeScript with Options API](https://vuejs.org/guide/typescript/options-api.html)
- [Vue.js Using Vue with TypeScript](https://vuejs.org/guide/typescript/overview.html)
reference/ts-strict-mode-options-api.md
---
title: Enable strict Mode for Proper Options API TypeScript Support
impact: HIGH
impactDescription: Without strict mode, 'this' in Options API components is typed as 'any', losing all type safety
type: gotcha
tags: [typescript, options-api, tsconfig, this-typing, configuration]
---

# Enable strict Mode for Proper Options API TypeScript Support

**Impact: HIGH** - Without `strict: true` (or at minimum `noImplicitThis: true`) in your tsconfig.json, the `this` context in Options API components is typed as `any`. This silently disables type checking for all property access on component instances.

## Task Checklist

- [ ] Enable `strict: true` in tsconfig.json (recommended)
- [ ] Or enable `noImplicitThis: true` at minimum
- [ ] Wrap components with `defineComponent()` for proper inference
- [ ] Verify type errors appear when accessing non-existent properties

## The Problem

TypeScript's default behavior without strict mode allows implicit `any` typing, which defeats the purpose of using TypeScript with Vue's Options API.

**tsconfig.json without strict mode:**
```json
{
  "compilerOptions": {
    "target": "ES2020"
    // No strict mode - this is DANGEROUS
  }
}
```

**Component with hidden type errors:**
```typescript
import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  methods: {
    increment() {
      // Without strict mode, these errors are SILENT:
      this.cont++          // Typo: should be 'count'
      this.nonExistent     // Property doesn't exist
      this.message.toFixed() // Wrong method for string
    }
  }
})
```

All of the above errors compile successfully without strict mode because `this` is implicitly `any`.

## Correct Configuration

**Recommended tsconfig.json:**
```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "jsx": "preserve",
    "isolatedModules": true
  }
}
```

**Minimum for Options API type safety:**
```json
{
  "compilerOptions": {
    "noImplicitThis": true
  }
}
```

## What strict Mode Enables

The `strict` flag is a shorthand for enabling multiple type-checking options:

| Option | Effect |
|--------|--------|
| `noImplicitThis` | Errors on `this` with implicit `any` type |
| `noImplicitAny` | Errors on expressions with implicit `any` type |
| `strictNullChecks` | null and undefined are distinct types |
| `strictFunctionTypes` | Stricter function parameter checking |
| `strictPropertyInitialization` | Class properties must be initialized |
| `strictBindCallApply` | Stricter bind, call, apply typing |
| `alwaysStrict` | Emits "use strict" in output |

## Correct Component with Proper Typing

```typescript
import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  computed: {
    doubleCount(): number {
      return this.count * 2  // 'this.count' is typed as number
    }
  },
  methods: {
    increment() {
      this.count++           // Type-safe: count is number
      // this.cont++         // ERROR: Property 'cont' does not exist
    },
    greet(name: string) {
      return `${this.message}, ${name}!`  // Type-safe
    }
  }
})
```

## Common Errors After Enabling Strict Mode

### Error: Property 'xxx' does not exist

```typescript
// Before: worked silently
this.unknownProp

// After: TypeScript error
// Property 'unknownProp' does not exist on type 'ComponentPublicInstance<...>'
```

Fix by adding the property to `data()` or declaring it properly.

### Error: Object is possibly 'undefined'

```typescript
methods: {
  getFirst() {
    const items = this.items
    // Error: Object is possibly 'undefined'
    return items[0].name
  }
}
```

Fix with proper null checks:
```typescript
methods: {
  getFirst() {
    return this.items?.[0]?.name
  }
}
```

## Important: defineComponent is Required

Even with strict mode, you must use `defineComponent()` to enable proper type inference:

```typescript
// BAD - No type inference for 'this'
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++  // 'this' is any even with strict mode!
    }
  }
}

// GOOD - Full type inference
import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++  // 'this.count' is properly typed as number
    }
  }
})
```

## Reference

- [Vue.js TypeScript Overview - tsconfig.json](https://vuejs.org/guide/typescript/overview.html#configuring-tsconfig-json)
- [Vue.js TypeScript with Options API](https://vuejs.org/guide/typescript/options-api.html)
- [TypeScript strict Mode](https://www.typescriptlang.org/tsconfig#strict)