Generics
Type parameters for reusable type-safe code, generic constraints with extends, and the standard utility types built on them.
12 questions
JuniorTheoryVery commonWhat are generics in TypeScript and why are they useful?
What are generics in TypeScript and why are they useful?
Generics parameterize a function, class, or type over a type variable so the same code works for many types while preserving the exact type. function identity<T>(x: T): T returns whatever type it receives, so identity(5) is typed number. Unlike any, which erases the type, a generic keeps the link between input and output, staying fully type-safe.
Common mistakes
- ✗Reaching for
anyinstead of a type parameter, losing the input-to-output type link - ✗Thinking generics exist at runtime — they are erased like all other types
- ✗Writing a separate overload per type when one generic would cover them all
Follow-up questions
- →How does a generic preserve type information that
anywould discard? - →Can the compiler infer the type argument, or must you always pass it?
JuniorTheoryVery commonWhat do Partial<T>, Required<T>, and Readonly<T> produce?
What do Partial<T>, Required<T>, and Readonly<T> produce?
Each is a mapped type over keyof T that flips one modifier. Partial<T> makes every property optional by adding ?, Required<T> removes optionality with -?, and Readonly<T> marks every property readonly. All three are shallow and all three are erased from the emitted JavaScript.
Common mistakes
- ✗Expecting them to recurse — all three are shallow, top-level only
- ✗Thinking
Readonly<T>freezes the object at runtime likeObject.freeze - ✗Forgetting
Required<T>exists and hand-writing a mapped type with-?
Follow-up questions
- →How would you write a deep
Readonlythat does recurse into nested objects? - →Which modifier does
-?remove, and what is the+form for?
JuniorTheoryCommonHow does TypeScript pick T when you call a generic function with no type arguments?
How does TypeScript pick T when you call a generic function with no type arguments?
The compiler infers T from the call's arguments: it matches each argument's type against the parameter that mentions T and picks the best common candidate, so identity(5) gives T = number. If T appears in no parameter, it falls back to its default or unknown.
Common mistakes
- ✗Believing an omitted type argument becomes
anyinstead of being inferred - ✗Expecting inference to run backwards from the annotation on the assignment target
- ✗Forgetting a parameter used only in the return type has no inference site at all
Follow-up questions
- →What is
Tinferred as when it appears only in the function's return type? - →How does a constraint on
Tinteract with what the compiler infers from the call?
MiddleCodeCommonType a memoize wrapper that keeps the wrapped function's arguments and result
Type a memoize wrapper that keeps the wrapped function's arguments and result
Be generic over the function type and rebuild the signature from it: (...args: Parameters<T>) => ReturnType<T>. Parameters<T> extracts the argument tuple, ReturnType<T> the result, so the cached version is checked exactly like the original.
Common mistakes
- ✗Returning
(...args: any[]) => any, which drops every check the original had - ✗Typing the wrapped function as
Function, soParameters<T>cannot be applied - ✗Forgetting that the cache key must be derived from the arguments, not from
T
Follow-up questions
- →How does
ReturnType<T>extract the result withinfer? - →What breaks if the wrapped function is overloaded?
MiddleTheoryCommonWhat does a generic parameter preserve that any throws away?
What does a generic parameter preserve that any throws away?
A generic keeps the link between input and output: in first<T>(xs: T[]): T the compiler binds T at the call site and hands the same type back, so first(users) is a User. With any[] the link is cut — the result is any and goes unchecked.
Common mistakes
- ✗Treating
<T>as documentation that the compiler replaces withany - ✗Annotating a helper with
any[]and then wondering why the result is unchecked - ✗Expecting
Tto be readable at run time after the call
Follow-up questions
- →How does
unknowndiffer fromanyas the parameter type here? - →What does adding a constraint
T extends { id: string }buy inside the body?
MiddleCodeCommonType a helper that accepts one item or an array of them and returns an array
Type a helper that accepts one item or an array of them and returns an array
Declare function toArray<T>(input: T | T[]): T[]. The compiler infers T from either branch — toArray(user) and toArray(users) both give User[]. Inside, Array.isArray(input) narrows the union, and no type argument is needed.
Common mistakes
- ✗Believing inference cannot resolve
Tthrough aT | T[]parameter - ✗Returning
T[] | T[][]because the union was not narrowed inside the body - ✗Reaching for overloads where a single generic union parameter is enough
Follow-up questions
- →What does
toArray(null)infer forT, and how would you exclude it? - →How would the signature change if the input could be
readonly T[]?
MiddleTheoryCommonHow do you constrain a generic type parameter in TypeScript?
How do you constrain a generic type parameter in TypeScript?
Use extends to require the type argument to be assignable to a given shape: function len<T extends { length: number }>(x: T) accepts only types that have a length, so x.length is allowed inside the body. The constraint narrows what callers may pass while still preserving the exact type. You can also constrain one parameter by another, e.g. <K extends keyof T> to index into T safely.
Common mistakes
- ✗Using
implementsinstead ofextendsto express a generic constraint - ✗Forgetting a constraint and then being unable to access members of
T - ✗Thinking
extendshere pinsTto one type rather than an upper bound
Follow-up questions
- →How does
<K extends keyof T>make indexed access type-safe? - →Can a constrained parameter still be inferred from the call arguments?
MiddleTheoryCommonWhat do the utility types Pick, Omit, and Exclude do?
What do the utility types Pick, Omit, and Exclude do?
Pick<T, K> builds a type with only the keys K from T. Omit<T, K> builds a type with every key of T except K — it is literally Pick<T, Exclude<keyof T, K>>. Exclude<U, E> operates on a union, removing the members of U that are assignable to E. So Pick and Omit select or drop object properties, while Exclude filters union members.
Common mistakes
- ✗Confusing
PickandOmit—Pickkeeps the listed keys,Omitdrops them - ✗Applying
Excludeto object keys instead of union members - ✗Thinking utility types run at runtime rather than being compile-time type operations
Follow-up questions
- →How is
Omit<T, K>expressed in terms ofPickandExclude? - →Which utility type would you use to make every property optional?
MiddleDesignOccasionalYou are reviewing a data-access layer. Some generic helpers are called with an explicit type argument at almost every call site — fetchOne<User>('/users/1'), parseBody<Order>(req) — while others are never given one and let inference do all the work. A teammate asks you for a rule the team can follow. Explain what actually makes a type parameter inferable, what the compiler does when a type parameter appears in no parameter position, and why an explicitly supplied type argument on such a helper is a different kind of claim from one the compiler inferred. Say where each style belongs.
You are reviewing a data-access layer. Some generic helpers are called with an explicit type argument at almost every call site — fetchOne<User>('/users/1'), parseBody<Order>(req) — while others are never given one and let inference do all the work. A teammate asks you for a rule the team can follow. Explain what actually makes a type parameter inferable, what the compiler does when a type parameter appears in no parameter position, and why an explicitly supplied type argument on such a helper is a different kind of claim from one the compiler inferred. Say where each style belongs.
A type parameter is inferable only if it occurs in a parameter. fetchOne<User>(url) has no User in its arguments, so the type argument is an unchecked claim about the response — a runtime schema belongs there. Elsewhere, let inference work.
Common mistakes
- ✗Reading an explicit
<User>on a fetch helper as a check rather than an assertion - ✗Expecting a type parameter that appears in no argument to be inferable
- ✗Writing the type argument at every call site even where the argument already carries it
Follow-up questions
- →What does the compiler infer when a type parameter appears in no parameter at all?
- →How does a runtime schema validator turn that claim into a real check?
MiddleTheoryOccasionalWhat do the in and out modifiers on a type parameter declare?
What do the in and out modifiers on a type parameter declare?
They declare the variance the compiler would otherwise infer: out T means T appears only in outputs (covariant), in T only in inputs (contravariant), in out T is invariant. The annotation is checked against real usage; a mismatch errors.
Common mistakes
- ✗Reading
in/outas a restriction on where the type argument may be passed - ✗Assuming the annotation is trusted rather than checked against real usage
- ✗Expecting the modifiers to have any effect at run time
Follow-up questions
- →What error do you get when
out Tis used in an input position? - →How did the compiler decide variance before these modifiers existed?
MiddleTheoryOccasionalWhy is (a: Animal) => void assignable to (d: Dog) => void, but not the reverse?
Why is (a: Animal) => void assignable to (d: Dog) => void, but not the reverse?
Function parameters are contravariant: a handler for any Animal also handles the Dog it will receive, so the wider parameter is safe. A Dog-only handler would break on a Cat. strictFunctionTypes enforces this on function-typed properties.
Common mistakes
- ✗Assuming parameters follow the same direction as the subtype relation on values
- ✗Believing
strictFunctionTypesalso makes method parameters contravariant - ✗Thinking a
voidreturn makes the parameter check irrelevant
Follow-up questions
- →Why does the same check stay bivariant for a method declared with shorthand syntax?
- →How is an array's element type checked, and why is that unsound?
SeniorTheoryRareWhy are method parameters checked bivariantly while function-property parameters are not?
Why are method parameters checked bivariantly while function-property parameters are not?
Deliberate unsoundness, kept for compatibility. strictFunctionTypes makes function-typed properties contravariant but exempts method shorthand — else Array<Dog> would not be an Array<Animal>. Declare the member as a property to get the check.
Common mistakes
- ✗Expecting
strictFunctionTypesto tighten method parameters as well - ✗Reading the bivariance as a bug rather than a compatibility decision
- ✗Not realizing that rewriting a method as a property changes how it is checked
Follow-up questions
- →Show a concrete unsound program that method bivariance lets through.
- →Why would making methods contravariant break
Array<T>assignability?