# Vue 3 with TypeScript in 2026: Type-Safe Props, Emits and Composables
> Master type-safe Vue 3 components with TypeScript: generic defineProps, tuple defineEmits, typed composables, defineModel and InjectionKey, plus interview questions.
- Published: 2026-07-07
- Updated: 2026-07-07
- Author: SharpSkill
- Tags: Vue 3, TypeScript, defineProps, Composables, Frontend
- Reading time: 8 min
---
Vue 3 with TypeScript in 2026 gives components fully static type checking across props, events, and reusable logic. The `
{{ props.name }}
{{ props.role }}
```
The `role` prop is now constrained to two literal strings, so passing `role="guest"` fails the build. This is the core reason to reach for `defineProps` with TypeScript: the [official Vue TypeScript guide](https://vuejs.org/guide/typescript/composition-api.html) treats the generic form as the default for `
```
Vue 3.5 stabilised **reactive props destructure**, which is now the more concise pattern. Destructuring the `defineProps` result and assigning a default in the same statement stays fully reactive — the compiler rewrites each access back to `props.x` under the hood.
```vue
{{ label }}
```
> **Reactivity trap with destructured props**
>
> Destructured props stay reactive in the template and in `computed`, but passing a destructured value straight into `watch` or a composable reads it once and drops the reactive link. Wrap it in a getter — `watch(() => color, ...)` — or convert it with `toRef(props, 'color')` when a ref is needed downstream.
This is a common interview trap: candidates assume the destructured variable is a plain value, when the compiler has actually rewired every read back to `props.color`.
## Type-safe emits with defineEmits
Events deserve the same rigour as props. The generic `defineEmits` describes each event name and its payload as a tuple, giving the parent component autocompletion and the child compile-time checks that the right arguments are emitted.
```vue
```
This tuple form replaced the older call-signature syntax (`(e: 'search', q: string): void`) because it reads better and supports multiple events without overloads. When an event carries no data, an empty tuple `[]` documents that explicitly. Pairing typed emits with typed props produces components whose entire public interface is verifiable before runtime — the same discipline covered in the [Vue composition API guide](/blog/vue-nuxt/vue-3-composition-api-complete-guide).
## Typing composables for reusable logic
Composables are plain functions, so they follow ordinary TypeScript rules — but a few conventions keep them ergonomic. Return `Ref` types explicitly when the inference is non-obvious, and use generics when a composable wraps arbitrary data such as an API response.
```ts
// useFetch.ts
import { ref, type Ref } from 'vue'
interface UseFetchReturn {
data: Ref
error: Ref
loading: Ref
}
// Generic flows through to the caller's typed data
export function useFetch(url: string): UseFetchReturn {
const data = ref(null) as Ref
const error = ref(null)
const loading = ref(true)
fetch(url)
.then((r) => r.json())
.then((json: T) => { data.value = json })
.catch((e: Error) => { error.value = e })
.finally(() => { loading.value = false })
return { data, error, loading }
}
```
At the call site the generic parameter makes `data` fully typed with zero extra annotation:
```ts
// UserList.vue (script setup)
interface User { id: number; name: string }
// data is Ref — inferred from the generic
const { data: users, loading } = useFetch('/api/users')
```
Explicitly annotating the return object (`UseFetchReturn`) is worth the few extra lines: it documents the contract, prevents accidental leaks of internal refs, and gives consumers a single type to import. For deeper composable patterns like argument overloads and lifecycle-aware cleanup, see the [advanced Vue composables guide](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns).
## Typing defineModel for two-way binding
`defineModel`, stable since Vue 3.4, collapses the old `modelValue` prop plus `update:modelValue` emit into a single writable ref. Its generic parameter types both directions of the binding at once.
```vue
```
Named models — `defineModel('title')` — map to `v-model:title` and receive the same typing. This eliminates an entire class of mismatched-payload bugs that the manual prop-and-emit pattern used to hide.
## Typing template refs and component instances
Accessing a DOM node or a child component through a `ref` is where untyped Vue code most often falls back to `any`. The fix is to parameterise `useTemplateRef` (Vue 3.5+) or the `ref` itself with the element type, so property access is checked against the real DOM interface.
```vue
```
For a reference to a child component, `InstanceType` extracts the component's public type, exposing whatever the child declared through `defineExpose`. This keeps parent-to-child method calls fully checked instead of guessed.
```vue
```
## Type-safe provide and inject with InjectionKey
Dependency injection across the component tree loses type information unless the key carries it. `InjectionKey` is a typed symbol that binds a value's type to its key, so `provide` and `inject` stay in sync without manual casting.
```ts
// theme-key.ts
import type { InjectionKey, Ref } from 'vue'
export interface ThemeContext {
mode: Ref<'light' | 'dark'>
toggle: () => void
}
// The key permanently associates the ThemeContext type with this symbol
export const ThemeKey: InjectionKey = Symbol('theme')
```
```ts
// provider (script setup) — value must match ThemeContext or it fails to compile
provide(ThemeKey, { mode, toggle })
// consumer — theme is inferred as ThemeContext | undefined
const theme = inject(ThemeKey)
theme?.toggle()
```
Providing a value whose shape does not match `ThemeContext` is a compile error at the injection site, catching drift between distant components before it ships.
## Common Vue TypeScript interview questions
Interviewers probe whether a candidate understands the boundary between compile-time types and runtime behaviour. A few recurring questions and their sharp answers:
- **Why prefer `defineProps()` over the runtime object form?** Generic props express union types, function signatures, and nested shapes that runtime declarations cannot, and they remove duplicated type/runtime definitions.
- **Are destructured props reactive?** Yes in Vue 3.5+, because the compiler rewrites access to `props.x`. But destructured *values* passed into `watch` or a composable are read once — use a getter or `toRef`.
- **How do you type an emitted event with no payload?** An empty tuple: `defineEmits<{ close: [] }>()`.
- **What does `defineModel` replace?** The `modelValue` prop and `update:modelValue` emit pair, unified into one typed writable ref.
Practising these against a real question bank sharpens the reflexes interviews reward — the [Vue composables interview module](/technologies/vue-nuxt/interview-questions/vue-composables) drills exactly these patterns. Tooling matters too: run `vue-tsc` in CI so type errors block merges, and lean on the [TypeScript handbook](https://www.typescriptlang.org/docs/handbook/2/generics.html) when composable generics get involved. The editor experience is powered by [Vue's official Volar tooling](https://github.com/vuejs/language-tools), which reads these macros to surface errors inline.
## Conclusion
- Use generic `defineProps()` to express union types, optional fields, and callback props that runtime declarations cannot capture.
- Prefer reactive props destructure with inline defaults in Vue 3.5+, and reach for `withDefaults` only when a shared defaults object is clearer.
- Type events with the tuple form of `defineEmits` so payloads are checked at compile time in both the child and the parent.
- Annotate composable return types explicitly and use generics to forward caller-supplied data types end to end.
- Adopt `defineModel()` for two-way binding to collapse the prop-plus-emit boilerplate into one typed ref.
- Run `vue-tsc` in continuous integration so type regressions fail the build rather than reaching production.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/en/blog/vue-nuxt/vue-3-typescript-type-safe-props-emits-composables