fix: convert apps/website from submodule to regular directory
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
---
|
||||
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
|
||||
- Need to enable TypeScript type inference for component properties → See [ts-options-api-use-definecomponent](reference/ts-options-api-use-definecomponent.md)
|
||||
- Enabling type safety for Options API this context → See [ts-strict-mode-options-api](reference/ts-strict-mode-options-api.md)
|
||||
- Using old TypeScript versions with prop validators → See [ts-options-api-arrow-functions-validators](reference/ts-options-api-arrow-functions-validators.md)
|
||||
- Event handler parameters need proper type safety → See [ts-options-api-type-event-handlers](reference/ts-options-api-type-event-handlers.md)
|
||||
- Need to type object or array props with interfaces → See [ts-options-api-proptype-complex-types](reference/ts-options-api-proptype-complex-types.md)
|
||||
- Injected properties missing TypeScript types completely → See [ts-options-api-provide-inject-limitations](reference/ts-options-api-provide-inject-limitations.md)
|
||||
- Complex computed properties lack clear type documentation → See [ts-options-api-computed-return-types](reference/ts-options-api-computed-return-types.md)
|
||||
|
||||
### Methods & Lifecycle
|
||||
- Methods aren't binding to component instance context → See [no-arrow-functions-in-methods](reference/no-arrow-functions-in-methods.md)
|
||||
- Lifecycle hooks losing access to component data → See [no-arrow-functions-in-lifecycle-hooks](reference/no-arrow-functions-in-lifecycle-hooks.md)
|
||||
- Debounced functions sharing state across component instances → See [stateful-methods-lifecycle](reference/stateful-methods-lifecycle.md)
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
---
|
||||
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)
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
---
|
||||
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)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
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)
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
---
|
||||
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/)
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
---
|
||||
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)
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
---
|
||||
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)
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
---
|
||||
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)
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
---
|
||||
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)
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
---
|
||||
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)
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user