Generics questions come in two flavours: "type this function so callers get the right type back" (practical, common) and "what does this conditional type evaluate to" (puzzle, less common). The senior skill is knowing when a generic preserves information and when it is showing off.
function first<T>(xs: T[]): T | undefined returns the element type the caller passed in; without the parameter you would return unknown. If a type parameter appears only once in a signature, it is usually unnecessary.T extends { id: string }) let you use properties inside the function while staying generic; defaults (T = string) apply when inference has nothing to go on. keyof T and T[K] type property access (get<T, K extends keyof T>(obj: T, key: K): T[K]).T from arguments, from the return position when contextual typing applies, and from as const/const T parameters for literals. NoInfer<T> (5.4) blocks a position from contributing candidates so a default value cannot widen the inferred type.T extends U ? X : Y) branch on types; over a naked type parameter they distribute across unions (ToArray<string | number> → string[] | number[]); wrap in a tuple ([T] extends [U]) to prevent distribution. infer extracts a type from a position (ReturnType, Awaited, element types).{ [K in keyof T]: ... } with modifiers (readonly, ?, -?) and key remapping via as (`on${Capitalize<K & string>}`), filtering keys with never. Template literal types build string unions and pair with Uppercase/Lowercase/Capitalize/Uncapitalize.Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, ReturnType, Parameters, ConstructorParameters, InstanceType, Awaited, NoInfer. Vue adds PropType, MaybeRef, MaybeRefOrGetter, ExtractPropTypes, ComponentExposed (from vue-component-type-helpers).strictFunctionTypes (for function-typed properties, not methods); this is why a callback accepting Animal can be passed where (dog: Dog) => void is expected but not vice versa. Overloads describe several call shapes; a union parameter is often simpler.type Json = string | number | boolean | null | Json[] | { [k: string]: Json }); deep utilities (DeepPartial) work but stress the checker.--extendedDiagnostics or --generateTrace. TypeScript 7's speed does not make quadratic types cheap.Practical generics an interviewer might ask you to type:
// 1. Preserve the element and key types: callers get back exactly what they passed in.
export function groupBy<T, K extends PropertyKey>(items: readonly T[], key: (item: T) => K): Record<K, T[]> {
const out = {} as Record<K, T[]>
for (const item of items) (out[key(item)] ??= []).push(item)
return out
}
// 2. Pick with autocomplete on keys and a precise result type.
export function pick<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K> {
return Object.fromEntries(keys.filter(k => k in obj).map(k => [k, obj[k]])) as Pick<T, K>
}
// 3. NoInfer: the default may not widen the inferred literal union.
export function createSelect<T extends string>(options: readonly T[], initial: NoInfer<T>) {
return { options, value: initial }
}
createSelect(['sm', 'md'], 'lg') // error: 'lg' is not 'sm' | 'md'; without NoInfer, T would widen to include 'lg'
// 4. Event map -> handler map with key remapping and template literals.
type Events = { select: { id: string }, close: void }
type Handlers = { [K in keyof Events as `on${Capitalize<K>}`]?: Events[K] extends void ? () => void : (payload: Events[K]) => void }
// { onSelect?: (payload: { id: string }) => void; onClose?: () => void }
// 5. Conditional + infer: unwrap the resolved data type of an async function.
type Data<F> = F extends (...args: never[]) => Promise<infer R> ? R : never
type Product = Data<typeof getProduct>
// 6. Distribution, and how to stop it.
type ToArray<T> = T extends unknown ? T[] : never // string | number -> string[] | number[]
type ToArrayAll<T> = [T] extends [unknown] ? T[] : never // string | number -> (string | number)[]
// 7. A typed path getter with template literal parsing (library-grade; keep it out of app code).
type PathValue<T, P extends string> = P extends `${infer Head}.${infer Rest}`
? Head extends keyof T ? PathValue<T[Head], Rest> : never
: P extends keyof T ? T[P] : never
export function get<T, P extends string>(obj: T, path: P): PathValue<T, P> {
return path.split('.').reduce<unknown>((acc, k) => (acc as Record<string, unknown>)?.[k], obj) as PathValue<T, P>
}
Variance in one example, because it explains a whole class of "why does this not type-check":
type Handler<E> = (event: E) => void
declare let onAnimal: Handler<Animal>
declare let onDog: Handler<Dog> // Dog extends Animal
onDog = onAnimal // ok: a handler that copes with any Animal certainly copes with a Dog
onAnimal = onDog // error under strictFunctionTypes: it might receive a Cat and call bark()
function log<T>(value: T): void gains nothing from T; unknown says the same and reads more honestly. Generics earn their place when the parameter appears at least twice, typically input and output.
type IsString<T> = T extends string ? true : false gives boolean for string | number, because it distributes and unions the results. When you mean "is the whole union a string", wrap both sides in tuples.
groupBy and pick from memory, then check them against the versions above; add a test file that must fail to compile for a wrong key.Handlers for a real component's emits and use it to type an options object.DeepPartial you find online, apply it to a 200-line interface, and measure the check time with --extendedDiagnostics before and after."I write a generic when a function must preserve information through it: first<T>(xs: T[]) gives back the element type, pick<T, K extends keyof T> gives autocomplete on keys and a precise result. The test is whether the type parameter appears at least twice, usually as input and output; if it appears once, unknown says the same thing honestly. It becomes over-engineering when callers always have to spell the parameter out, when one helper is generic for one call site, or when a conditional-type puzzle replaces a small union and a plain function. Deep recursive or template-literal types belong in libraries; in application code they cost editor speed and reviewer patience."
"A conditional type branches on a type relation: T extends U ? X : Y. When T is a naked type parameter and you pass a union, the conditional distributes, evaluating per member and unioning the results, which is why ToArray<string | number> is string[] | number[]; wrapping both sides in a tuple turns distribution off. infer declares a type variable inside the extends clause and captures whatever sits in that position, which is how ReturnType, Awaited and element-type extraction are written. Together with mapped types and key remapping they let me derive one type from another, for instance an onSelect handler map from an events map, so the two cannot drift apart."
Nothing yet. Notes filed here show up on this page and the section overview.
Type system fundamentals
Structural typing, unknown versus any versus never, unions and intersections, literal types and as const, narrowing and type guards, satisfies, readonly and tuples, enums versus unions, and the strictness flags that matter.
TypeScript with Vue and Nuxt
Typing props, emits, slots and models through the compiler macros, generic components, typing refs and injection, Nuxt's generated types — auto-imports, components, typed routes, typed $fetch, runtimeConfig and app.config augmentation — and where declarations must live.