AcademyVue
Vue with TypeScript: Generics & Type Guards
A generic <T> only constrains what a function accepts — a type guard is the one construct that actually checks a value's shape at runtime.
2 min readUpdated 3 exercises
A generic type parameter is a contract about the signature, not the body
function uniqueBy<T>(items: T[], key: (item: T) => string): T[] {
const seen = new Set<string>()
const result: T[] = []
for (const item of items) {
const fingerprint = key(item) // or a bug: JSON.stringify(item) instead
if (!seen.has(fingerprint)) {
seen.add(fingerprint)
result.push(item)
}
}
return result
}<T> here says "this function works uniformly for any type you call it with" — nothing more. It says nothing about what the function actually does with T internally. A bug where the implementation fingerprints by JSON.stringify(item) instead of by the caller's own key(item) selector is not a type error at all: both are valid strings, so the code compiles and runs, deduping by the wrong criterion. Generics buy you reuse across types; they do not audit correctness within one call.
unknown is the type that forces you to check before you use
Data crossing a real boundary — a JSON response, localStorage, a query param — should be typed unknown, not cast immediately to whatever shape you expect:
function parseUser(raw: unknown): User {
return raw as User // "trust me" — nothing is actually checked
}as User convinces the type checker and nothing else. At runtime, raw is whatever it actually is — if that is { id: 2 } with no name, the cast produces a "User" whose .name is undefined, and nothing about the cast itself ever surfaces that.
A type guard is a function both the type checker and the runtime believe
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as Record<string, unknown>).id === 'number' &&
typeof (value as Record<string, unknown>).name === 'string'
)
}
function parseUser(raw: unknown): User | null {
return isUser(raw) ? raw : null
}The value is User return type is what makes this different from a function that merely returns boolean — inside an if (isUser(raw)) branch, or in the ternary above, TypeScript narrows raw's type to User for the rest of that branch, because the guard's body genuinely inspected the value's shape rather than asserting it.
What interviewers check
Whether as gets named as a real risk, not a routine step — every as cast is a promise to the compiler with nothing behind it at runtime, and the strongest answer is not "avoid as entirely" (sometimes it is the only option) but knowing exactly which casts are load-bearing and validating the ones that cross a real trust boundary.
Now practice it
1 quiz · 2 drillsReading this page is the cheap half. These are the exercises that make you use it.
- Generic Functions & ComposablesProA generic uniqueBy<T>() that ignores its own key selector and dedupes by the whole object shape instead.DrillSolve · HardDrillSolve · Hard
- Type GuardsProparseUser() blindly casts unknown data to User — fixed with a real value is User type guard.DrillSolve · HardDrillSolve · Hard
- Composition API · Advanced ComposablesGeneric composables and validated data both show up in the harder end of this Composition API question set.QuizTake · HardQuizTake · Hard
Read next
Vue with TypeScript: Props & Emits
A discriminated union prop or a typed emits object only pays off once the component actually branches and matches on it.
2 min
Provide / Inject: Skipping the Prop Chain
provide() and inject() pass data straight to any descendant, no matter how deep — but only stay reactive when what you provide is itself reactive.
2 min