\n```\n\nWithDefaults helps you provide default values while keeping type ", "url": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i#step-2" }, { "@type": "HowToStep", "position": 3, "name": "Typing Emits and Event Payloads", "text": "Emits can be typed so parent components get correct payload hints. Define an emits validator or use generic helper types.\n\nExample:\n\n```ts\nimport { defineComponent } from 'vue'\n\nexport default defineComponent({\n emits: {\n 'update': (payload: { value: number }) => typeof payload.value === 'number'\n },\n setup(_, { emit }) {\n const updateValue = (n: number) => emit('update', { value: n })\n return { updateValue }\n }\n})\n```\n\nFor more advanced callback patterns and function types, our art", "url": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i#step-3" }, { "@type": "HowToStep", "position": 4, "name": "Typing Refs, Reactive, and Computed", "text": "Ref and reactive need explicit generic types when inference is insufficient. Use Ref to annotate variables that hold primitives or objects.\n\nExample:\n\n```ts\nimport { ref, reactive, computed, Ref } from 'vue'\n\nconst count: Ref = ref(0)\nconst state = reactive({ items: [] as string[] })\nconst doubled = computed(() => count.value * 2)\n\n// Type safety: state.items is string[] and count.value is number\n```\n\nIf you work with array methods, good typing helps avoid mistakes; review patterns fo", "url": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i#step-4" }, { "@type": "HowToStep", "position": 5, "name": "Creating Typed Composition Functions", "text": "Reusable composition functions should expose typed inputs and outputs to preserve developer ergonomics.\n\nExample:\n\n```ts\nimport { ref, computed, Ref } from 'vue'\n\nexport function useCounter(initial = 0) {\n const count: Ref = ref(initial)\n const increment = () => { count.value += 1 }\n const double = computed(() => count.value * 2)\n return { count, increment, double } as const\n}\n\n// useCounter() inferred return types allow autocompletion in components\n```\n\nFor more advanced generics in", "url": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i#step-5" }, { "@type": "HowToStep", "position": 6, "name": "Typing provide / inject", "text": "Provide and inject work well with TypeScript when you centralize keys and types.\n\nExample:\n\n```ts\nimport { provide, inject, InjectionKey, ref } from 'vue'\n\ninterface AuthContext { userId: string | null }\nconst authKey: InjectionKey = Symbol('auth')\n\nexport function provideAuth() {\n const ctx = { userId: null }\n provide(authKey, ctx)\n}\n\nexport function useAuth() {\n const ctx = inject(authKey)\n if (!ctx) throw new Error('Auth not provided')\n return ctx\n}\n```\n\nUsing InjectionKey e", "url": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i#step-6" }, { "@type": "HowToStep", "position": 7, "name": "Working with Third-Party Libraries and Untyped Modules", "text": "When a dependency lacks types, you have a few options: install community types, write minimal ambient declarations, or wrap the API with a typed adapter.\n\nExample ambient declaration:\n\n```ts\ndeclare module 'untyped-lib' {\n export function createThing(options: any): any\n}\n```\n\nHowever, aim to create a small typed facade that maps the untyped API to a typed interface. This helps keep the rest of your codebase strictly typed. Learn how to organize types and modules in larger apps in our article on", "url": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i#step-7" }, { "@type": "HowToStep", "position": 8, "name": "Typing Async Operations and API Responses", "text": "When fetching data, annotate promise results and use typed interfaces for payloads to avoid shape surprises.\n\nExample:\n\n```ts\nimport { ref } from 'vue'\n\ninterface ApiUser { id: string; name: string }\n\nasync function fetchUser(id: string): Promise {\n const res = await fetch(`/api/users/${id}`)\n const data = await res.json() as ApiUser\n return data\n}\n\nconst user = ref(null)\n\nfetchUser('1').then(u => user.value = u)\n```\n\nFor patterns and pitfalls when typing promises and", "url": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i#step-8" }, { "@type": "HowToStep", "position": 9, "name": "Leveraging Generics for Scalable Components", "text": "Generic components allow you to create highly reusable building blocks. Use TypeScript generics with defineComponent or within composition functions.\n\nExample:\n\n```ts\nimport { defineComponent, PropType } from 'vue'\n\nexport default defineComponent({\n props: {\n items: { type: Array as PropType, required: true } as any\n }\n})\n```\n\nThe complexity here comes from ensuring the generic T is declared and inferred correctly. For many cases, it's easier to type the composition function around a g", "url": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i#step-9" }, { "@type": "HowToStep", "position": 10, "name": "Debugging Type Errors and Common Fixes", "text": "Typical problems include incorrect union assignments, missing properties, or wrong ref types. A frequent error message is 'argument of type X is not assignable to parameter of type Y'. When you encounter this, walk the types backward, and consult our troubleshooting article on [resolving the \"argument of type 'X' is not assignable to parameter of type 'Y'\" error in TypeScript](/typescript/resolving-the-argument-of-type-x-is-not-assignable) for concrete fixes.\n\nCommon fixes:\n- Add or relax generi", "url": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i#step-10" } ] }, { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.codefixeshub.com/" }, { "@type": "ListItem", "position": 2, "name": "TypeScript", "item": "https://www.codefixeshub.com/topics/typescript" }, { "@type": "ListItem", "position": 3, "name": "Typing Vue.js Components with the Composition API in TypeScript", "item": "https://www.codefixeshub.com/typescript/typing-vuejs-components-with-the-composition-api-i" } ] }, { "@context": "https://schema.org", "@type": "Organization", "name": "CodeFixesHub", "alternateName": "Code Fixes Hub", "url": "https://www.codefixeshub.com", "logo": { "@type": "ImageObject", "url": "https://www.codefixeshub.com/CodeFixesHub_Logo_Optimized.png", "width": 600, "height": 60 }, "description": "Expert programming solutions, code fixes, and tutorials for developers. Find solutions to common coding problems and learn new technologies.", "foundingDate": "2024", "founder": { "@type": "Person", "name": "Parth Patel" }, "contactPoint": { "@type": "ContactPoint", "contactType": "customer service", "url": "https://www.codefixeshub.com/contact" }, "sameAs": [ "https://github.com/codefixeshub", "https://twitter.com/codefixeshub" ], "knowsAbout": [ "JavaScript", "TypeScript", "React", "Node.js", "Python", "Programming", "Web Development", "Software Engineering" ] } ]
    CodeFixesHub
    programming tutorial

    Typing Vue.js Components with the Composition API in TypeScript

    Master typing Vue 3 Composition API components with TypeScript. Hands-on examples, props/emits typing, generics, and tips — start typing confidently today.

    article details

    Quick Overview

    TypeScript
    Category
    Oct 1
    Published
    19
    Min Read
    2K
    Words
    article summary

    Master typing Vue 3 Composition API components with TypeScript. Hands-on examples, props/emits typing, generics, and tips — start typing confidently today.

    Typing Vue.js Components with the Composition API in TypeScript

    Introduction

    As Vue 3 and the Composition API gain adoption, writing well-typed components with TypeScript becomes essential for building maintainable, reliable UIs. Intermediate developers often feel confident with basic TypeScript and Vue setup but stumble when typed props, emits, refs, provides, generics, and reusable composition functions enter the picture. This guide fills that gap with a practical, example-driven approach.

    You will learn how to type props and emits correctly, create fully typed setup return values, leverage generics in composition utilities, and integrate TypeScript patterns that scale across an app. We cover common errors and show how to avoid them, plus offer troubleshooting tips for problems like 'argument of type X is not assignable' and incorrect ref types.

    By the end of this tutorial you will be able to write Composition API components that provide strong editor feedback, reduce runtime bugs, and make refactors safer. Examples include defineComponent usage, the script setup macro, typed provide/inject, and patterns for reusable composition functions. The techniques here apply to single-file components, utilities, and larger codebases.

    Background & Context

    Vue 3's Composition API focuses on logical reusability and clearer separation of concerns. TypeScript complements the Composition API by giving you static guarantees about the shapes of props, the return types of composition functions, and the events a component emits. Combining both allows teams to enforce contracts between components and detect mistakes during development rather than at runtime.

    Typing Composition API code is different from class or Options API patterns. Instead of relying on class decorators or implicit typings, you define explicit types for props, events, and refs. This requires familiarity with TypeScript features like generics, utility types, and the Ref type. For many patterns, concepts from React typing are helpful; if you want to dive into component typing ideas, see our guide on typing function components for relevant patterns.

    Key Takeaways

    • How to type props in defineComponent and script setup using PropType and withDefaults
    • Typing component emits and ensuring downstream consumers get correct event payload types
    • Correctly typing refs, reactive, and computed values using Ref and Reactive types
    • Reusable, generic composition functions with proper inference and ReturnType usage
    • Strategies to handle third-party untyped libs and JSON data using interfaces or type aliases
    • Common TypeScript errors in Vue patterns and how to fix them with practical examples

    Prerequisites & Setup

    Before following the examples, ensure you have:

    • Vue 3 and TypeScript installed in your project
    • A Vite or Vue CLI project configured for TypeScript
    • Editor support for TypeScript (VS Code recommended)

    Recommended tsconfig flags include strict mode options that catch many errors early. For a suggested set of flags and migration tips, see our guide on recommended tsconfig.json strictness flags. Install types for Vue with package manager and enable the script setup macro if you prefer that syntax.

    Main Tutorial Sections

    1) Typing Basic Props with PropType

    Vue props need explicit typing when the type is more complex than a primitive. Use PropType from 'vue' to annotate complex shapes.

    Example:

    ts
    import { defineComponent, PropType } from 'vue'
    
    interface User {
      id: string
      name: string
      age?: number
    }
    
    export default defineComponent({
      props: {
        user: {
          type: Object as PropType<User>,
          required: true
        }
      },
      setup(props) {
        // props.user is correctly typed as User
        return {}
      }
    })

    If you prefer the script setup macro, use defineProps with the same PropType pattern. Choosing between interfaces and type aliases for incoming JSON or payloads is common; our guide on typing JSON data using interfaces or type aliases helps decide the right form.

    2) Using script setup and withDefaults

    The script setup macro simplifies syntax but needs type annotations for props to get full benefits.

    Example:

    vue
    <script setup lang='ts'>
    import { withDefaults, defineProps } from 'vue'
    
    interface Options { theme?: string }
    
    const props = withDefaults(defineProps<{ id: string; options?: Options }>(), {
      options: () => ({ theme: 'light' })
    })
    
    // props.id is string, props.options is Options with default applied
    </script>

    WithDefaults helps you provide default values while keeping type inference for props intact. Defaults must match declared types to avoid assignment errors.

    3) Typing Emits and Event Payloads

    Emits can be typed so parent components get correct payload hints. Define an emits validator or use generic helper types.

    Example:

    ts
    import { defineComponent } from 'vue'
    
    export default defineComponent({
      emits: {
        'update': (payload: { value: number }) => typeof payload.value === 'number'
      },
      setup(_, { emit }) {
        const updateValue = (n: number) => emit('update', { value: n })
        return { updateValue }
      }
    })

    For more advanced callback patterns and function types, our article on typing callbacks in TypeScript illustrates common approaches you can reuse within emit handlers.

    4) Typing Refs, Reactive, and Computed

    Ref and reactive need explicit generic types when inference is insufficient. Use Ref to annotate variables that hold primitives or objects.

    Example:

    ts
    import { ref, reactive, computed, Ref } from 'vue'
    
    const count: Ref<number> = ref(0)
    const state = reactive({ items: [] as string[] })
    const doubled = computed(() => count.value * 2)
    
    // Type safety: state.items is string[] and count.value is number

    If you work with array methods, good typing helps avoid mistakes; review patterns for typing array methods for map/filter/reduce examples that transfer to computed lists.

    5) Creating Typed Composition Functions

    Reusable composition functions should expose typed inputs and outputs to preserve developer ergonomics.

    Example:

    ts
    import { ref, computed, Ref } from 'vue'
    
    export function useCounter(initial = 0) {
      const count: Ref<number> = ref(initial)
      const increment = () => { count.value += 1 }
      const double = computed(() => count.value * 2)
      return { count, increment, double } as const
    }
    
    // useCounter() inferred return types allow autocompletion in components

    For more advanced generics in utilities, use ReturnType and generic parameters to keep inference intact across modules.

    6) Typing provide / inject

    Provide and inject work well with TypeScript when you centralize keys and types.

    Example:

    ts
    import { provide, inject, InjectionKey, ref } from 'vue'
    
    interface AuthContext { userId: string | null }
    const authKey: InjectionKey<AuthContext> = Symbol('auth')
    
    export function provideAuth() {
      const ctx = { userId: null }
      provide(authKey, ctx)
    }
    
    export function useAuth() {
      const ctx = inject(authKey)
      if (!ctx) throw new Error('Auth not provided')
      return ctx
    }

    Using InjectionKey ensures injected values carry type information. Avoid using plain symbols without types when possible.

    7) Working with Third-Party Libraries and Untyped Modules

    When a dependency lacks types, you have a few options: install community types, write minimal ambient declarations, or wrap the API with a typed adapter.

    Example ambient declaration:

    ts
    declare module 'untyped-lib' {
      export function createThing(options: any): any
    }

    However, aim to create a small typed facade that maps the untyped API to a typed interface. This helps keep the rest of your codebase strictly typed. Learn how to organize types and modules in larger apps in our article on organizing your TypeScript code.

    8) Typing Async Operations and API Responses

    When fetching data, annotate promise results and use typed interfaces for payloads to avoid shape surprises.

    Example:

    ts
    import { ref } from 'vue'
    
    interface ApiUser { id: string; name: string }
    
    async function fetchUser(id: string): Promise<ApiUser> {
      const res = await fetch(`/api/users/${id}`)
      const data = await res.json() as ApiUser
      return data
    }
    
    const user = ref<ApiUser | null>(null)
    
    fetchUser('1').then(u => user.value = u)

    For patterns and pitfalls when typing promises and async/await flows, see our guide on typing asynchronous JavaScript.

    9) Leveraging Generics for Scalable Components

    Generic components allow you to create highly reusable building blocks. Use TypeScript generics with defineComponent or within composition functions.

    Example:

    ts
    import { defineComponent, PropType } from 'vue'
    
    export default defineComponent({
      props: {
        items: { type: Array as PropType<T[]>, required: true } as any
      }
    })

    The complexity here comes from ensuring the generic T is declared and inferred correctly. For many cases, it's easier to type the composition function around a generic and return typed helpers to the component.

    10) Debugging Type Errors and Common Fixes

    Typical problems include incorrect union assignments, missing properties, or wrong ref types. A frequent error message is 'argument of type X is not assignable to parameter of type Y'. When you encounter this, walk the types backward, and consult our troubleshooting article on resolving the "argument of type 'X' is not assignable to parameter of type 'Y'" error in TypeScript for concrete fixes.

    Common fixes:

    • Add or relax generic constraints with extends
    • Narrow types with type guards at runtime
    • Use as casting sparingly and prefer narrow interfaces

    Advanced Techniques

    Once you are comfortable with basic typing, use these expert patterns to improve maintainability:

    • Use explicit ReturnType in components to avoid drifting types across refactors
    • Create typed factories for provides using InjectionKey to ensure strictness across feature modules
    • Use mapped types for normalized stores or complex form models
    • Build small typed adapters around untyped libs instead of sprinkling any across the codebase
    • Consider centralizing shared DTO types for API responses and reference them across frontend and backend if possible

    For a deeper exploration of organizing types and maintaining a scalable codebase, consult the article on best practices for writing clean and maintainable TypeScript code. Also consider the tradeoffs between read-only types and runtime immutability libraries in our piece on using readonly vs immutability libraries.

    Best Practices & Common Pitfalls

    Dos:

    • Prefer explicit types for props and public composition function APIs
    • Enable strict TypeScript settings early to catch errors early; check recommended tsconfig flags
    • Keep types small and focused: split large interfaces into composable pieces
    • Use InjectionKey for provide/inject patterns to preserve type info

    Don'ts:

    • Don’t overuse any to silence compiler errors; instead, refine types or write narrow adapters
    • Avoid implicit any in public APIs that are consumed across modules
    • Don’t rely on runtime checks alone; combine runtime validators with TypeScript types when validating external data

    Troubleshooting tips:

    • When emits produce unexpected types, ensure your emits validator matches the payload shape
    • If props inference fails in script setup, explicitly annotate the defineProps call
    • For common array and object method typing issues, review patterns in typing array methods

    Real-World Applications

    Typed Composition API components matter most in medium-to-large applications where multiple developers interact with shared UI primitives, forms, and data models. Examples:

    • A design system: typed components ensure consumers use props and slots correctly
    • Form libraries: typing form state, validation functions, and submission payloads prevents incorrect payloads from reaching the server
    • Feature modules: typed provide/inject enables safe cross-feature communication

    For patterns when typing Mongoose models and backend DTOs consumed by a Vue frontend, understanding how to type schemas and models can be useful; see our primer on typing Mongoose schemas and models to align backend and frontend types.

    Conclusion & Next Steps

    Typing Vue 3 components with the Composition API pays dividends in reliability and maintainability. Start by typing props, emits, and refs in your smallest components, then extract typed composition functions and shared DTOs. Next, tighten your tsconfig settings and gradually increase strictness to catch regressions early. Explore the linked articles for deep dives into callbacks, async patterns, and organization strategies.

    Recommended next steps:

    • Add typing to a small component and write unit tests that exercise typed behaviors
    • Extract a reusable composition function and document its types
    • Adopt stricter tsconfig flags from the referenced guide and fix resulting type errors iteratively

    Enhanced FAQ

    Q: How should I decide between interface and type alias for component props or API data?

    A: Both work, but interfaces are extensible and better for public, extendable shapes. Type aliases are more flexible for unions and mapped types. If you model JSON payloads returned by an API, see typing JSON data using interfaces or type aliases to choose based on extension needs and patterns.

    Q: Can I rely on Vue's inference or should I always specify types explicitly?

    A: Vue offers robust inference for many simple cases, but explicit types prevent silent regressions and improve editor experience. For public APIs like props, emits, and composition returns, prefer explicit typing. When in doubt, annotate the public surface and allow locals to infer.

    Q: How do I type a generic composition function that works with different models?

    A: Use TypeScript generics and constrain them when necessary. Example:

    ts
    export function useList<T>(initial: T[] = []) {
      const items = ref<T[]>(initial)
      const add = (t: T) => items.value.push(t)
      return { items, add }
    }

    Return types remain inferred where you consume the hook, preserving autocompletion.

    Q: What is the correct way to type emits to ensure parent components get proper types?

    A: Define the emits object with validators or create a typed emits helper signature. The validator approach is recommended because it pairs runtime guard with type annotation. You can also use generics and helper types to map event names to payload shapes.

    Q: Why do I sometimes see 'This expression is not callable' or similar runtime type complaints in Vue + TypeScript?

    A: Those errors usually indicate you attempted to treat a value as a function or call something that TypeScript thinks is not callable. Often the root cause is incorrect generics or missing ReturnType. Our article on fixing the "This expression is not callable" error in TypeScript has general strategies to diagnose and fix such issues.

    Q: How should I handle untyped third-party Vue plugins or components?

    A: Create a small typed wrapper that exposes only the properties and methods you need, or write minimal ambient module declarations as a stopgap. Prefer wrapping and mapping to typed interfaces for long-term maintainability.

    Q: Are there special considerations for typing slots and scoped slots in the Composition API?

    A: Yes. Typed slots are best expressed through component generic patterns or by defining explicit slot props on the child component's props interface. When building design system components, provide clear slot prop types so consumers get accurate hints.

    Q: How do I approach typing async operations and handling API errors gracefully?

    A: Always type the expected success payload with an interface or type alias and model possible error shapes. Use discriminated unions for responses that may be success or error. For promise and async typing patterns, consult our guide on typing asynchronous JavaScript.

    Q: What are common pitfalls when migrating an existing Vue 2 TypeScript codebase to Vue 3 Composition API types?

    A: Common pitfalls include mismatched prop shapes, reliance on global 'this', and implicit any creeping into composition functions. Adopt strict tsconfig flags incrementally, and consider refactoring small modules to composition functions with explicit types. For broader organization guidance, see organizing your TypeScript code.

    Q: Where can I find tips for improving TypeScript developer experience while typing Vue components?

    A: Use well-scoped interfaces, prefer explicit prop/emits typing, centralize shared DTOs, and use editor plugins that surface type information. Additionally, read our article on best practices for writing clean and maintainable TypeScript code for general strategies that directly improve DX.

    Additional Resources

    With these patterns and the linked deep dives, you should be well-equipped to type real-world Vue 3 Composition API components and scale typing across your application.

    article completed

    Great Work!

    You've successfully completed this TypeScript tutorial. Ready to explore more concepts and enhance your development skills?

    share this article

    Found This Helpful?

    Share this TypeScript tutorial with your network and help other developers learn!

    continue learning

    Related Articles

    Discover more programming tutorials and solutions related to this topic.

    No related articles found.

    Try browsing our categories for more content.

    Content Sync Status
    Offline
    Changes: 0
    Last sync: 11:19:51 PM
    Next sync: 60s
    Loading CodeFixesHub...