In a Vue 3.5 `<script setup lang="ts">` component, how do you declare a generic item type `T`, and how is it chosen at each usage?
answer
- an attribute on the script tag
- same syntax as a TS parameter list
- inferred from the props a parent passes
- a comment pins it when inference fails
- erased before the browser sees it
basics
~20 sAdd a generic attribute to the script tag, for example generic="T extends { id: string }", and use T in defineProps and defineEmits. Each parent usage infers T from the props it passes; the parameter exists only for type checking.
solid answer
~50 sSince Vue 3.3 you declare type parameters with the `generic` attribute: `<script setup lang="ts" generic="T extends { id: string | number }">`. Its value is parsed exactly like the part between `<` and `>` in a TypeScript signature, so it takes constraints, several comma-separated parameters, default types and imported types. Inside the block `T` is usable in `defineProps<{ items: T[] }>()`, `defineEmits<{ select: [item: T] }>()` and local code. The Vue language tools (behind `vue-tsc` and the editor) treat the SFC as a generic function, so in `<List :items="users" />` TypeScript infers `T = User` from the props and checks every emit and slot against it. When props cannot determine `T`, a `<!-- @vue-generic {User} -->` comment before the tag pins it. At runtime nothing is generic: the SFC compiler ignores the attribute, and a prop typed as bare `T` gets no runtime type check.
code
vue · 15 lines<script setup lang="ts">
import List from './List.vue'
interface User { id: number; name: string }
const users: User[] = [{ id: 1, name: 'Ada' }]
function onSelect(user: User) {
console.log(user.name)
}
</script>
<template>
<!-- T is inferred as User from :items -->
<List :items="users" @select="onSelect" />
</template>go deeper
Recall that a script setup block becomes generic through a generic attribute on the script tag, and that T is then usable in defineProps.
Explain that the attribute is a TypeScript parameter list, that T is inferred per usage from the props, and that @vue-generic pins it when inference fails.
Point out that generics are erased: the SFC compiler ignores the attribute and a bare-T prop has no runtime check, so vue-tsc in CI is the real guard.
Weigh a generic component against a loose one typed with unknown: generics move errors to each call site but add inference edge cases your team must understand.
## What the `generic` attribute does A **generic component** is a component whose props, events, slots and exposed API share a type that is chosen **per usage** rather than fixed by the author: a list of users, a list of products and a list of invoices can all use the same `List` component and still be fully typed. Since **Vue 3.3**, a `<script setup lang="ts">` block declares its type parameters through a `generic` attribute on the `<script>` tag. The attribute value works exactly like the parameter list between `<` and `>` in a TypeScript function signature; Vue adds no syntax of its own. ## A generic List in practice ```vue <script setup lang="ts" generic="T extends { id: string | number }"> defineProps<{ items: T[] selected?: T }>() const emit = defineEmits<{ select: [item: T] }>() </script> <template> <ul> <li v-for="item in items" :key="item.id" @click="emit('select', item)"> {{ item.id }} </li> </ul> </template> ``` The constraint `T extends { id: string | number }` is what lets the template read `item.id` for the `key`; with a bare `T`, the template type check would reject `item.id` because nothing promises that property exists. ## How `T` gets its value at each usage 1. The **Vue language tools** (the engine behind `vue-tsc` and the official editor extension) generate a virtual TypeScript version of the SFC in which the component is a generic function, roughly `<T extends { id: string | number }>(props, ctx, expose) => ...`. 2. When a parent writes `<List :items="users" @select="onSelect" />`, TypeScript **infers** `T` from the props passed, here `User`. 3. Every other place typed with `T` follows that inference: `onSelect` must accept a `User`, `selected` must be a `User`, and any typed slot props receive `User`. 4. If the argument violates the constraint (say `items` is `number[]`), the error appears on that prop in the **parent's** template. When inference has nothing to work with, for example because `T` appears in no prop, or the props are typed too loosely, the parent can pin the type explicitly with a comment placed immediately before the component tag: ```vue <template> <!-- @vue-generic {import('@/api').Actor} --> <ApiSelect v-model="peopleIds" endpoint="/api/actors" /> </template> ``` Templates have no `<List<User>>` syntax; the `@vue-generic` comment is the escape hatch. ## Why not type the items as `unknown`? The alternative to a generic is a component typed with a fixed, loose type such as `items: unknown[]` or `items: Array<{ id: string | number }>`. That compiles, but the looseness leaks into every parent: - the `select` handler receives `unknown` (or the bare constraint), so every parent must cast or re-narrow; - nothing links the type of `selected` to the type of `items`, so a parent can pass a product as the selection of a user list; - slot props and exposed methods lose the connection too. A generic keeps one type flowing through all of them, and the price is paid once, in the child's declaration. That is why interviewers treat the generic attribute as the Vue-native answer to a reusable, strongly typed list, table or select component. ## What the attribute accepts | Form | Example | Note | |---|---|---| | Single parameter | `generic="T"` | `T` is unconstrained, so the template can do little with it | | Constraint | `generic="T extends string \| number"` | Standard TypeScript `extends` | | Several parameters | `generic="T extends string \| number, U extends Item"` | Comma-separated, as in a signature | | Default type | `generic="T = string"` | Used when nothing else determines `T` | | Imported type | `U extends Item` with `import type { Item } from './types'` in the block | The import lives in the script body | ## What happens at runtime The generic parameter is a **compile-time contract only**: - The SFC compiler (`@vue/compiler-sfc`) does not read the `generic` attribute. It still converts the `defineProps` type into runtime prop declarations by walking the type's syntax. - `items: T[]` becomes an `Array` prop, because the array shape is visible in the syntax. - A prop typed as bare `T` cannot be resolved to a runtime constructor, so it is emitted with type `null`, which means **no runtime type check**. - The browser therefore never warns that a wrong item type was passed; `vue-tsc` or the editor is what catches it. ## Common pitfalls - **Expecting runtime validation** of `T`: it does not exist; run `vue-tsc` in CI if the contract matters. - **Leaving `T` unconstrained** and then reading properties of it in the template. - **Writing `<List<User>>`** in a template: not valid template syntax; use `@vue-generic`. - **A type parameter no prop mentions**: nothing can infer it, so it falls back to its default or constraint unless pinned. - **Confusing it with `defineProps<T>()`**: the type argument of `defineProps` is the props type itself, not a declaration of a type parameter.
- What does a Vue generic component's T resolve to if no prop mentions it?Nothing at the usage site gives TypeScript a candidate, so `T` falls back to its default type if the `generic` attribute declares one, otherwise to its constraint (or `unknown`). Emits and slot props typed with `T` then become loose. Either make a prop carry `T` or pin it in the parent with a `<!-- @vue-generic {Type} -->` comment before the tag.
- Can a generic Vue component use T in defineEmits as well as defineProps?Yes. Everything inside the `<script setup>` block is in scope of the type parameter, so `defineEmits<{ select: [item: T] }>()` types the payload with the same `T` the parent's props fixed. A parent handler that expects a different type is then a type error in the parent's template.
The generic attribute is a blank on a pre-printed shipping label: each sender fills it in by what they put in the box (the props), the sorting office (the type checker) checks the label, and the delivery van (the runtime) never reads that blank at all.
saying these in an interview costs you the question
- Script setup cannot be generic; you must move the component into a .ts file.
- Vue validates T at runtime and warns when a parent passes the wrong item type.
- The parent chooses the type by writing <List<User>> in its template.
- The generic attribute only accepts bare names, never extends constraints or several parameters.
- defineProps<T>() by itself declares T as the component's type parameter.