skip to content

What do the `in`, `out` and `in out` variance annotations on a TypeScript generic type parameter do, and why add them when the compiler already works variance out from usage?

level: seniorimportance: nice to knowfreq 25%

answer

  1. optional modifiers on a type parameter
  2. producer and consumer, spelled as keywords
  3. verified against usage, not obeyed
  4. a shortcut past the structural comparison
  5. measured cost, not a default habit

basics

~20 s

They state a type parameter's variance explicitly: out means covariant, in means contravariant, in out means invariant. TypeScript verifies the declaration against how the parameter is used, and can then relate two instantiations by their type arguments instead of comparing members.

solid answer

~50 s

They are optional declarations of variance, added in TypeScript 4.7. `out T` says the parameter is covariant and may appear only in output positions, `in T` says contravariant and input positions only, and `in out` says invariant. Crucially they do not *change* anything — TypeScript already derives variance structurally, and the compiler checks your annotation against the actual member usage, erroring if you write `out` on a parameter you then accept as an argument. So the reasons to add them are documentation of intent, and checker performance: comparing two instantiations of a large or recursive generic member by member is expensive, and a declared variance lets the checker relate them by comparing the type arguments alone. That shortcut is also the reason to be sparing — skipping the structural comparison can surface an error in an edge case where the member-by-member check would have succeeded.

code

typescript · 18 lines
typescript
class Animal { name = ""; }
class Dog extends Animal { bark() {} }

interface Getter<out T> { get: () => T }
interface Setter<in T> { set: (value: T) => void }
interface Ref<in out T> { get: () => T; set: (value: T) => void }

declare const dogGetter: Getter<Dog>;
const animalGetter: Getter<Animal> = dogGetter; // covariant, as declared

declare const animalSetter: Setter<Animal>;
const dogSetter: Setter<Dog> = animalSetter; // contravariant, as declared

declare const dogRef: Ref<Dog>;
// const animalRef: Ref<Animal> = dogRef;      // invariant: rejected both ways
// interface Wrong<out T> { set: (v: T) => void } // error: T used in an input position

console.log(animalGetter.get().name, dogSetter, dogRef);

go deeper

for a junior

Know that these modifiers exist on generic type parameters and that out means the parameter is only produced while in means it is only consumed.

for a middle

Explain that TypeScript infers variance from usage by default and that the annotation is checked against the members, so writing out on a parameter used as an argument is a compile error rather than an override.

for a senior

Give both motivations — freezing intent on a public type and cutting the cost of structural comparison in deeply generic code — and note that relying on the fast path can change which assignments are accepted.

for a principal

Set the policy: variance is part of a shared type's published contract, so decide where annotations are mandatory, and treat blanket annotation as a change to checking behaviour that needs the same care as any other API change.

## What the syntax says Since TypeScript 4.7 a type parameter of a generic interface or type alias may carry a variance modifier: ```ts interface Getter<out T> { get: () => T } interface Setter<in T> { set: (value: T) => void } interface Ref<in out T> { get: () => T; set: (value: T) => void } ``` - `out T` — **covariant**. `T` may appear only in output positions, and `Getter<Dog>` is assignable to `Getter<Animal>`. - `in T` — **contravariant**. `T` may appear only in input positions, and `Setter<Animal>` is assignable to `Setter<Dog>`. - `in out T` — **invariant**. `T` appears in both kinds of position, and only the same type argument is assignable. The keywords match the producer/consumer intuition directly: a covariant parameter is one that only comes *out* of the type, a contravariant one only goes *in*. ## They describe, they do not decide This is the point interviewers are usually probing. TypeScript's default behaviour is to measure variance from usage — the annotation is not what makes a type covariant. Write the annotation and the compiler checks it against the members, reporting an error on the annotation when the two disagree: ```ts interface Wrong<out T> { set: (value: T) => void } // error: T is used contravariantly ``` So an annotation cannot rescue an invariant type. If your container both reads and writes `T`, marking it `out` does not grant it covariance; it produces a compile error. The only way to get covariant assignability is to change the members so the parameter really is only produced. ## Why they exist at all There are two honest reasons to reach for them. **Intent.** In a large public type, `out T` states a contract to future maintainers: this parameter is produced, never consumed. If someone later adds a member that accepts a `T`, the annotation turns what would have been a silent variance change — and a wave of assignability errors at distant call sites — into an immediate, local compile error on the type they just edited. That is a genuinely valuable guard rail on a widely depended-on type. **Checker performance.** To decide whether two instantiations of a generic relate, the checker normally compares them structurally, member by member, instantiated with the respective type arguments. For a deeply nested or recursive generic — the kind that shows up in type-level libraries, builder chains, and heavily generic component props — that comparison can be very expensive, and it happens over and over during a build. A declared variance lets the checker take a shortcut: if the parameter is known covariant, relating `C<A>` to `C<B>` reduces to relating `A` to `B`, with no member walk at all. ## The cost of the shortcut That shortcut is exactly why these annotations should not be sprinkled everywhere. The structural comparison and the variance-based comparison are not always identical in their verdicts: structural comparison sometimes succeeds in cases the coarser variance rule rejects. When you declare the variance, you are telling the checker it may rely on the fast path, so an assignment that used to be accepted by the member-by-member fallback can begin to report an error. The change is correct in the sense that it follows the declared variance, but it is a behaviour change in your codebase, not merely a comment. The practical rule: leave variance inferred by default; add annotations to a small number of hot, deeply generic types where you have measured a type-checking cost, or to a stable public type whose variance you want frozen as part of its contract. ## What they are not Three things worth stating plainly, because each is a common wrong answer: - They emit nothing. Like every other part of the type layer, variance annotations are erased; the JavaScript output is byte-identical with or without them. - They are not the same feature as declaration-site variance being *required*. In languages where variance must be declared, an unannotated parameter is invariant by default. In TypeScript the unannotated case is fully inferred, so annotations are additive. - They do not affect inference of the type argument. `out` does not mean "infer this from the return type", and `in` does not steer argument inference; controlling inference is a different mechanism entirely. ## How to talk about it A strong answer names the three forms, says explicitly that TypeScript verifies rather than obeys them, and gives the two motivations — intent and check performance — before conceding that the fast path can change results in edge cases. A weak answer describes them as the way you make a generic covariant, which inverts the entire relationship between the annotation and the type.

  • If a type is invariant because it reads and writes T, can `out T` make it covariant?
    No. The compiler checks the annotation against the members and reports an error, because `T` appears in an input position that `out` forbids. Annotations assert variance the type already has; the only way to obtain covariance is to remove the consuming member, typically by extracting a read-only view of the type.
  • Do variance annotations change the emitted JavaScript?
    No — they are part of the type layer and are erased along with the type parameters themselves. Output is identical with or without them. The only observable effects are compile-time: the compiler enforces the declared variance on the members, and may use it to relate instantiations faster.
  • Is there a downside to annotating everything `out` where it happens to be true?
    Yes. Declaring variance lets the checker relate instantiations by type argument instead of falling back to a structural comparison, and the two do not always agree — some assignments the member-by-member check accepted can start erroring. Annotate deliberately, for stable public contracts or measured check-time hot spots.

saying these in an interview costs you the question

  • Says `out` is how you make a generic covariant
  • Claims the annotations change the emitted output
  • Thinks they control how type arguments are inferred
  • Believes unannotated parameters default to invariant in TypeScript
  • Recommends annotating every type parameter for speed

context