# 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 ` ``` 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 ``` > **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