Define Properties with Vue Property Decorator and TypeScript

Build type-safe Vue components using class syntax and decorators. Learn how @Prop, @Watch, @Emit, and @Ref transform component development.

Introduction to Vue Property Decorator

Vue Property Decorator provides a declarative approach to building Vue components using TypeScript decorators. This library enables developers to define component properties, methods, computed values, and lifecycle hooks using class syntax and TypeScript type annotations, resulting in more maintainable and type-safe Vue applications.

By leveraging TypeScript's type system alongside Vue's reactivity, teams can catch errors at compile time while enjoying cleaner, more expressive component definitions. The decorators available in Vue Property Decorator cover the full spectrum of Vue component options.

The library works alongside vue-class-component to provide decorator-based syntax for defining Vue components. The decorators exported include @Prop for defining component inputs, @Watch for reactive watchers, @Emit for event emission, @Ref for accessing child component references, @Model for two-way binding support, and @Provide/@Inject for dependency injection patterns.

For developers looking to enhance their TypeScript tooling setup, check out our guide on essential VS Code extensions for TypeScript development to maximize your productivity when building Vue applications.

Vue Property Decorator GitHub

Core Decorators Overview

The key decorators that transform how you build Vue components

@Prop

Define component input properties with full TypeScript type inference and validation options.

@Ref

Type-safe access to child component references and DOM elements without any casts.

@Watch

Create reactive watchers that execute callbacks when observed data changes.

@Emit

Mark methods as event emitters with automatic payload handling and event naming.

Setting Up Vue Property Decorator

Getting started with Vue Property Decorator requires a few essential dependencies. First, ensure TypeScript is configured with experimental decorator support enabled in the tsconfig.json file. The compiler options must include "experimentalDecorators": true and "useDefineForClassFields": false to ensure proper decorator behavior with class fields.

TypeScript decorators documentation

Required dependencies:

npm install vue vue-class-component vue-property-decorator --save

tsconfig.json configuration:

{
 "compilerOptions": {
 "experimentalDecorators": true,
 "useDefineForClassFields": false,
 "module": "esnext",
 "moduleResolution": "node",
 "strict": true
 }
}

Vue Property Decorator builds upon vue-class-component by providing additional decorators that wrap and extend the base functionality. The vue-class-component library provides the @Component decorator that registers the class as a Vue component, while Vue Property Decorator adds specialized decorators for individual component options.

Understanding module systems and modern JavaScript patterns is crucial for Vue development. Our guide on ES Modules in Node.js covers the fundamentals that power modern module resolution in Vue applications.

Vue Property Decorator GitHub

Using @Prop for Component Inputs

The @Prop decorator declares a component input property, equivalent to the props option in the Options API. This decorator supports TypeScript type inference, meaning the type annotation on the class property determines the prop type without requiring explicit type configuration. The decorator accepts an optional options object for additional configuration such as required flags, default values, and validator functions.

Basic prop declaration:

import { Component, Prop, Vue } from 'vue-property-decorator'

@Component
export default class UserCard extends Vue {
 @Prop() readonly name!: string
 @Prop({ type: Number }) readonly age?: number
 @Prop({ type: String, required: true }) readonly email!: string
}

The ! TypeScript non-null assertion operator indicates that the prop is guaranteed to be provided when the component is used, avoiding strict null checks while maintaining type safety. Optional props use the ? operator, while required props include the required: true option in the decorator configuration.

Prop validation:

@Prop({
 type: Number,
 validator: (value: number) => value >= 0 && value <= 120
}) readonly age?: number

@Prop({
 type: Array,
 default: () => [] // Factory function required for arrays/objects
}) readonly tags!: string[]

Important caveat: When providing default values for object or array props, always use a factory function that returns a new instance. This prevents shared mutable state across component instances that would occur if using a literal object or array directly.

For comprehensive testing strategies for your Vue components with proper prop validation, see our guide on unit and integration testing for Node.js applications that covers testing patterns applicable to Vue components.

Vue Property Decorator GitHub

TypeScript documentation

Using @Ref for Component References

The @Ref decorator provides type-safe access to child component instances and DOM elements, replacing the traditional this.$refs approach with proper TypeScript typing. This enables IDE autocomplete and compile-time checking when accessing referenced components or elements.

Child component references:

import { Component, Ref, Vue } from 'vue-property-decorator'
import ChildComponent from './ChildComponent.vue'

@Component
export default class ParentComponent extends Vue {
 @Ref() readonly childComponent!: ChildComponent

 handleClick() {
 this.childComponent.doSomething()
 const data = this.childComponent.componentData
 }
}

DOM element references:

import { Component, Ref, Vue } from 'vue-property-decorator'

@Component
export default class InputComponent extends Vue {
 @Ref('submitButton') readonly submitBtn!: HTMLButtonElement

 mounted() {
 console.log(this.submitBtn.offsetHeight)
 this.submitBtn.focus()
 }
}
<template>
 <div>
 <child-component ref="childComponent" />
 <button ref="submitButton">Submit</button>
 </div>
</template>

The ref attribute in the template must match the string identifier passed to @Ref(), creating a reliable connection between the template reference and the TypeScript class property. For performance optimization techniques when working with component references, learn about debugging React performance issues--the concepts apply similarly to Vue's reactivity system.

Vue Property Decorator GitHub

Using @Model for Two-Way Binding

The @Model decorator enables custom two-way data binding by declaring model props that automatically create corresponding events. This follows Vue's custom v-model pattern, allowing components to participate in two-way binding with explicit event naming conventions.

import { Component, Model, Vue } from 'vue-property-decorator'

@Component
export default class ToggleSwitch extends Vue {
 @Model('change', { type: Boolean }) readonly isChecked!: boolean

 toggle() {
 this.$emit('change', !this.isChecked)
 }
}

Usage with v-model automatically connects the isChecked property with the change event:

<toggle-switch v-model="isEnabled" />

When the component emits the change event with a new value, Vue updates the bound data. Conversely, when the bound data changes externally, the component receives the updated value through the model prop. For more complex scenarios, the @Model decorator supports all standard prop options including type, required, and validator configurations.

Understanding dynamic imports and code splitting patterns for Vue applications can significantly improve initial load times. Our guide on dynamic imports and code splitting in Next.js covers strategies that translate well to Vue applications.

Vue Property Decorator GitHub

Reactive Data and Computed Properties

Defining Data Properties

In Vue Property Decorator, class properties automatically become reactive data properties without any decorator requirement. When a property is declared on the class, Vue's reactivity system tracks it, and changes to the property automatically trigger re-renders and watcher callbacks.

import { Component, Vue } from 'vue-property-decorator'

@Component
export default class Counter extends Vue {
 count: number = 0
 message: string = 'Hello, Vue!'
 items: string[] = []

 increment() {
 this.count++
 }

 addItem(item: string) {
 this.items.push(item)
 }
}

This behavior relies on TypeScript's class field declarations and Vue's reactivity system initialization. The useDefineForClassFields: false compiler option ensures Vue can properly intercept property access and mutation.

Vue Property Decorator GitHub

Defining Computed Properties

The @Computed decorator creates derived state that automatically updates when its dependencies change. Computed properties cache their results and only recalculate when dependencies update, making them efficient for derived data calculations.

import { Component, Computed, Vue } from 'vue-property-decorator'

@Component
export default class UserList extends Vue {
 users: User[] = []

 @Computed()
 get activeUserCount(): number {
 return this.users.filter(user => user.isActive).length
 }

 @Computed()
 get sortedUsers(): User[] {
 return [...this.users].sort((a, b) =>
 a.name.localeCompare(b.name)
 )
 }
}

Computed properties can also be defined using a factory function for more complex configurations. Best practices for computed properties include keeping them pure and avoiding side effects, as computed values may be cached and only recalculated when dependencies change.

Methods and Watchers

Using @Watch for Reactive Watchers

The @Watch decorator creates reactive watchers that execute callback functions when observed data changes. Watchers are essential for side effects, data transformation, and external integrations that need to respond to data changes.

import { Component, Watch, Vue } from 'vue-property-decorator'

@Component
export default class SearchInput extends Vue {
 searchQuery: string = ''

 @Watch('searchQuery')
 onSearchQueryChanged(newValue: string, oldValue: string): void {
 console.log(`Query changed from "${oldValue}" to "${newValue}"`)
 this.debounceSearch()
 }

 @Watch('searchQuery', { immediate: true })
 onSearchQueryImmediate(newValue: string): void {
 if (newValue) {
 this.performSearch(newValue)
 }
 }

 @Watch('items', { deep: true })
 onItemsChanged(newItems: Item[]): void {
 this.saveToLocalStorage()
 }
}

The { deep: true } option enables watching nested object and array changes, triggering the callback when any property within the observed structure changes. The { immediate: true } option causes the watcher to execute immediately when the component is created, with the initial value as both the new and old value.

Class methods automatically become component methods, accessible through this within the component instance. Methods can be standard functions or async functions returning promises, enabling straightforward integration with external APIs and asynchronous operations.

Vue Property Decorator GitHub

Event Handling with @Emit

The @Emit decorator marks methods as event emitters, automatically forwarding return values or arguments as event payloads. This provides type-safe event communication between components, replacing $emit() calls with decorator-based declarations.

import { Component, Emit, Vue } from 'vue-property-decorator'

@Component
export default class ButtonComponent extends Vue {
 @Emit('click')
 handleClick(event: MouseEvent): MouseEvent {
 return event
 }

 @Emit('update:modelValue')
 updateValue(value: string): string {
 return value
 }

 @Emit()
 submitForm(): void {
 // Event named 'submit-form'
 }

 @Emit('custom-event')
 sendData(data: { id: number; name: string }): { id: number; name: string } {
 return data
 }
}

Event naming conventions follow Vue's established patterns. The @Emit('event-name') decorator explicitly names the event, while default naming converts camelCase method names to kebab-case events. Payload handling depends on the method's return type: methods returning a value emit that value as the payload, while void methods emit nothing unless parameters are specified.

When multiple arguments need to be passed as payload, returning an array allows the receiving component to destructure the values. Alternatively, explicit arguments can be specified using @Emit('event-name', ['arg1', 'arg2']) syntax.

Vue Property Decorator GitHub

Lifecycle Hooks

Class components can define lifecycle hooks by declaring methods with specific names that Vue automatically calls at corresponding points in the component lifecycle. These hooks work identically to the Options API equivalents while benefiting from TypeScript's type checking.

import { Component, Vue } from 'vue-property-decorator'

@Component
export default class LifecycleDemo extends Vue {
 created(): void {
 console.log('Component created')
 this.initializeData()
 }

 mounted(): void {
 console.log('Component mounted to DOM')
 this.setupEventListeners()
 }

 beforeDestroy(): void {
 console.log('Component before destruction')
 this.cleanup()
 }

 private initializeData(): void { }
 private setupEventListeners(): void {
 window.addEventListener('resize', this.handleResize)
 }
 private cleanup(): void {
 window.removeEventListener('resize', this.handleResize)
 }
}

Vue 3 renamed some lifecycle hooks for clarity: beforeDestroy became beforeUnmount and destroyed became unmounted. When using Vue Property Decorator with Vue 3, ensure compatibility by checking the installed version and using appropriate hook names. The library supports both Vue 2 and Vue 3 with appropriate version selections.

Lifecycle hooks can be async, enabling direct use of await within the hook method. This simplifies setup and teardown logic that depends on asynchronous operations.

Vue Property Decorator GitHub

Best Practices and Performance

Type Safety and Developer Experience

Leveraging TypeScript with Vue Property Decorator provides significant developer experience benefits including compile-time error detection, intelligent autocomplete, and refactoring support. Understanding how to maximize these benefits requires attention to several key patterns.

Recommended patterns:

// Good: TypeScript types with minimal Vue configuration
@Prop({ required: true }) readonly userId!: number
@Prop() readonly status?: 'active' | 'inactive' | 'pending'

// Good: Explicit type annotation for refs
@Ref() readonly formValidator!: FormValidatorComponent

String literal types and discriminated unions enable type-safe prop validation at compile time. When props can only take specific string values, defining them as literal types prevents invalid values from being passed to components.

Component references using @Ref should include proper TypeScript type annotations to enable autocomplete and prevent incorrect property access. Computed properties should be pure functions without side effects, as they may be cached and called multiple times without tracking dependencies.

Performance Considerations

Vue's reactivity system has inherent overhead that developers should understand when building class-style components. Each reactive property incurs memory and computation costs for tracking dependencies and triggering updates.

  • Limit reactive data to what the component actually needs
  • Use computed properties for expensive calculations (they cache results)
  • Deep watchers can be expensive for large objects
  • Consider non-reactive data for read-only static configurations

Computed properties provide automatic caching, recalculating only when dependencies change. For expensive computations, prefer computed properties over methods or watchers that execute on every render or change. Component structure affects both performance and maintainability--large components benefit from decomposition into smaller, focused components.

Comparison with Composition API

When to Use Class-Style Components

The Composition API introduced in Vue 3 offers an alternative approach to organizing component logic that addresses some limitations of the class-based approach while providing different trade-offs. Understanding when each approach suits a project helps make informed architectural decisions.

Class-style example:

@Component
export default class UserProfile extends Vue {
 @Prop({ required: true }) readonly userId!: number
 @Ref() readonly avatar!: HTMLImageElement

 @Computed()
 get user(): User | null {
 return userStore.getUserById(this.userId)
 }

 @Watch('user', { immediate: true })
 onUserChanged(user: User | null): void {
 this.trackProfileView(user)
 }
}

Composition API with script setup:

<script setup lang="ts">
const props = defineProps<{ userId: number }>()
const avatar = ref<HTMLImageElement>()
const user = computed(() => userStore.getUserById(props.userId))

watch(() => props.userId, (newId) => {
 trackProfileView(userStore.getUserById(newId))
}, { immediate: true })
</script>

Class-style components with Vue Property Decorator excel in scenarios where developers prefer object-oriented patterns, need explicit structure for team onboarding, or work with codebases that benefited from vue-class-component patterns. The Composition API with <script setup> provides a more flexible approach with improved TypeScript support.

Both approaches are valid and supported in Vue 3. The choice depends on team preferences, existing codebase patterns, and specific project requirements. Migration between approaches is possible but requires significant effort. For new projects, evaluate team familiarity and long-term maintenance implications when choosing between decorator-based and composition-based patterns.

Conclusion

Vue Property Decorator provides a mature, well-documented approach to building type-safe Vue components using TypeScript class syntax. The library's decorator-based API maps cleanly to Vue's component options while leveraging TypeScript's type system for improved developer experience and error prevention.

Key benefits include:

  • Compile-time type checking for props and component references
  • IDE autocomplete and intelligent suggestions
  • Clear visual structure through decorator annotations
  • Familiar object-oriented patterns for team transitions

For teams already using Vue 2 or preferring class-based patterns, Vue Property Decorator remains a solid choice. For new Vue 3 projects, evaluating the Composition API alongside class components helps select the approach that best fits team skills and project requirements.

Looking to build type-safe Vue applications? Our web development services team specializes in modern Vue.js development with TypeScript, helping you create maintainable, performant web applications that scale.

Frequently Asked Questions

Build Type-Safe Vue Applications

Our team specializes in modern Vue.js development with TypeScript, helping you create maintainable, performant web applications.