
Vue Options Api Best Practices
- 2.2k installs
- 2.8k repo stars
- Updated May 30, 2026
- hyf0/vue-skills
This is a copy of vue-options-api-best-practices by vuejs-ai - installs and ranking accrue to the original listing.
vue-options-api-best-practices is a Vue agent skill that prevents common Options API mistakes breaking component context and causing runtime errors for developers writing Vue 2 or Vue 3 components with the Options API.
About
vue-options-api-best-practices is an agent skill tagged HIGH impact that warns against using arrow functions for Options API lifecycle hooks in Vue 2 and Vue 3. Arrow functions lexically bind this from the enclosing scope, so Vue cannot bind this to the component instance in mounted, created, and similar hooks, leaving this undefined or pointing at the wrong context. That breaks access to component data, methods, and computed properties at runtime. The skill covers options-api lifecycle patterns with tags including vue3, vue2, lifecycle, arrow-functions, this-binding, mounted, and created. Developers reach for vue-options-api-best-practices when debugging mysterious undefined this errors in Options API components or reviewing legacy Vue codebases that mix arrow and function syntax in hooks.
- Prevents `this` binding failures in lifecycle hooks
- Enforces regular function syntax or ES6 method shorthand for created, mounted, updated, unmounted
- Allows arrow functions only for nested callbacks inside hooks
- HIGH impact rule set that eliminates undefined context bugs
- Task checklist that agents can apply during code generation or review
Vue Options Api Best Practices by the numbers
- 2,191 all-time installs (skills.sh)
- +33 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hyf0/vue-skills --skill vue-options-api-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.2k |
|---|---|
| repo stars | ★ 2.8k |
| Security audit | 3 / 3 scanners passed |
| Last updated | May 30, 2026 |
| Repository | hyf0/vue-skills ↗ |
Why is this undefined in Vue lifecycle hooks?
Prevent common Vue Options API mistakes that break component context and cause runtime errors.
Who is it for?
Vue developers using the Options API in Vue 2 or Vue 3 who hit undefined this errors in mounted, created, or other lifecycle hooks.
Skip if: Projects standardized on Vue Composition API with setup() where Options API lifecycle this-binding rules do not apply.
When should I use this skill?
The developer uses Vue Options API lifecycle hooks, sees undefined this at runtime, or asks about arrow functions in mounted or created.
What you get
Correct Options API lifecycle hook syntax with function declarations that preserve component this binding.
- Corrected lifecycle hook patterns
- Options API review guidance
By the numbers
- Tagged HIGH impact for arrow function lifecycle hook mistakes
Files
Vue.js Options API best practices, TypeScript integration, and common gotchas.
TypeScript
- Need to enable TypeScript type inference for component properties → See ts-options-api-use-definecomponent
- Enabling type safety for Options API this context → See ts-strict-mode-options-api
- Using old TypeScript versions with prop validators → See ts-options-api-arrow-functions-validators
- Event handler parameters need proper type safety → See ts-options-api-type-event-handlers
- Need to type object or array props with interfaces → See ts-options-api-proptype-complex-types
- Injected properties missing TypeScript types completely → See ts-options-api-provide-inject-limitations
- Complex computed properties lack clear type documentation → See ts-options-api-computed-return-types
Methods & Lifecycle
- Methods aren't binding to component instance context → See no-arrow-functions-in-methods
- Lifecycle hooks losing access to component data → See no-arrow-functions-in-lifecycle-hooks
- Debounced functions sharing state across component instances → See stateful-methods-lifecycle
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:
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:
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:
beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeUnmount(Vue 3) /beforeDestroy(Vue 2)unmounted(Vue 3) /destroyed(Vue 2)activateddeactivatederrorCapturedrenderTrackedrenderTriggered
Reference
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:
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:
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
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:
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:
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
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
validatoranddefaultprop 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:
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:
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):
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
# Check installed TypeScript version
npx tsc --version
# Or check package.json
grep typescript package.jsonReference
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
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:
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:
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:
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:
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:
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
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
PropTypefrom 'vue' for complex prop types - [ ] Use
as PropType<YourType>afterObject,Array, orFunction - [ ] 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:
// 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:
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:
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
props: {
// Optional object that can be null
selectedItem: {
type: Object as PropType<Item | null>,
default: null
}
}Enum-like Props
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
props: {
// Accept string, number, or object with id
value: {
type: [String, Number, Object] as PropType<string | number | { id: string }>,
required: true
}
}Event Handler Props
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:
// 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
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
// Provider component (parent)
import { defineComponent } from 'vue'
export default defineComponent({
provide() {
return {
theme: 'dark',
user: { id: 1, name: 'John' }
}
}
})// 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:
// types/injections.d.ts
import 'vue'
declare module 'vue' {
interface ComponentCustomProperties {
theme: 'light' | 'dark'
user: { id: number; name: string }
}
}
export {}// 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:
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
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: truein tsconfig.json to catch implicit any errors
The Problem
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
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:
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
<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:
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:
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
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
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
defineComponentfrom 'vue' for Options API components - [ ] Enable
strict: true(or at minimumnoImplicitThis: 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:
// 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:
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:
// 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:
// 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
<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:
// Copied from JS - MISSING defineComponent!
export default {
name: 'MyComponent',
// ... entire component without type inference
}Always add defineComponent when converting to TypeScript.
Reference
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: truein tsconfig.json (recommended) - [ ] Or enable
noImplicitThis: trueat 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:
{
"compilerOptions": {
"target": "ES2020"
// No strict mode - this is DANGEROUS
}
}Component with hidden type errors:
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:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"isolatedModules": true
}
}Minimum for Options API type safety:
{
"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
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
// 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'
methods: {
getFirst() {
const items = this.items
// Error: Object is possibly 'undefined'
return items[0].name
}
}Fix with proper null checks:
methods: {
getFirst() {
return this.items?.[0]?.name
}
}Important: defineComponent is Required
Even with strict mode, you must use defineComponent() to enable proper type inference:
// 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
Related skills
How it compares
Pick vue-options-api-best-practices over general Vue skills when debugging Options API this-binding failures in lifecycle hooks specifically.
FAQ
Why should Vue lifecycle hooks avoid arrow functions?
Vue Options API lifecycle hooks should use regular functions because arrow functions lexically bind this from the outer scope. Vue cannot attach the component instance as this, so mounted and created hooks fail when accessing data, methods, or computed properties.
Does vue-options-api-best-practices apply to Composition API?
vue-options-api-best-practices targets the Options API in Vue 2 and Vue 3 where lifecycle hooks rely on instance this binding. Composition API code using setup() follows different patterns and is outside this skill's scope.
Is Vue Options Api Best Practices safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.