Bird
Raised Fist0
Typescriptprogramming~7 mins

Covariance and contravariance in Typescript

Choose your learning style10 modes available
Introduction

Covariance and contravariance help control how types relate when you use them in functions or collections. They make sure your program stays safe when you replace one type with another.

When you want to pass a more specific type where a general type is expected.
When you want to return a more specific type from a function that promises a general type.
When you want to accept function parameters that can handle more general types safely.
When working with arrays or collections that hold related types.
When designing APIs that accept or return functions with flexible type rules.
Syntax
Typescript
interface Producer<T> {
  produce: () => T;
}

interface Consumer<T> {
  consume: (item: T) => void;
}

TypeScript uses structural typing and does not have explicit in or out keywords like some languages, but it supports covariance and contravariance in function types.

Return types are usually covariant (you can return a subtype), and parameter types are contravariant (you can accept a supertype).

Examples
You can assign a function returning a Dog to a variable expecting a function returning an Animal because Dog has all Animal properties.
Typescript
type Animal = { name: string };
type Dog = { name: string; bark: () => void };

// Covariance example:
let getAnimal: () => Animal;
let getDog: () => Dog = () => ({ name: 'Rex', bark: () => console.log('Woof') });

getAnimal = getDog; // Allowed: Dog is subtype of Animal
You can assign a function that accepts an Animal to a variable expecting a function that accepts a Dog, because Dog is more specific.
Typescript
type Animal = { name: string };
type Dog = { name: string; bark: () => void };

// Contravariance example:
let feedDog: (dog: Dog) => void;
let feedAnimal: (animal: Animal) => void = (animal) => console.log(animal.name);

feedDog = feedAnimal; // Allowed: feedAnimal accepts any Animal, so it can feed a Dog
Arrays are covariant, so you can assign a Dog array to an Animal array variable, but this can cause runtime errors if you add incompatible types.
Typescript
let animals: Animal[] = [{ name: 'Cat' }];
let dogs: Dog[] = [{ name: 'Rex', bark: () => console.log('Woof') }];

// Arrays are covariant in TypeScript
animals = dogs; // Allowed but can be unsafe if you add a Cat to animals later
Sample Program

This program shows covariance by assigning a function returning Dog to a variable expecting a function returning Animal. It also shows contravariance by assigning a function accepting Animal to a variable expecting a function accepting Dog.

Typescript
type Animal = { name: string };
type Dog = { name: string; bark: () => void };

function getAnimal(): Animal {
  return { name: 'Generic Animal' };
}

function getDog(): Dog {
  return { name: 'Rex', bark: () => console.log('Woof') };
}

let animalProducer: () => Animal;
let dogProducer: () => Dog = getDog;

// Covariance: dogProducer can be assigned to animalProducer
animalProducer = dogProducer;

console.log(animalProducer().name); // Prints 'Rex'

function feedAnimal(animal: Animal) {
  console.log(`Feeding ${animal.name}`);
}

let dogFeeder: (dog: Dog) => void;

// Contravariance: feedAnimal can be assigned to dogFeeder
dogFeeder = feedAnimal;

dogFeeder({ name: 'Buddy', bark: () => console.log('Woof') });
OutputSuccess
Important Notes

Covariance means you can use a more specific type where a general type is expected, usually in return types.

Contravariance means you can use a more general type where a specific type is expected, usually in function parameters.

TypeScript checks these rules mostly in function types to keep your code safe.

Summary

Covariance lets you replace a type with a more specific one safely, mainly in outputs.

Contravariance lets you replace a type with a more general one safely, mainly in inputs.

Understanding these helps you write flexible and safe TypeScript code with functions and collections.

Practice

(1/5)
1. Which statement best describes covariance in TypeScript?
easy
A. It allows replacing a type with a more specific type safely, mainly in outputs.
B. It allows replacing a type with a more general type safely, mainly in inputs.
C. It means types cannot be replaced or changed in any way.
D. It only applies to primitive types like string and number.

Solution

  1. Step 1: Understand covariance meaning

    Covariance means you can use a more specific type where a general type is expected, usually for outputs like return values.
  2. Step 2: Compare with contravariance

    Contravariance is about inputs and generalizing types, so it is different from covariance.
  3. Final Answer:

    It allows replacing a type with a more specific type safely, mainly in outputs. -> Option A
  4. Quick Check:

    Covariance = more specific output type [OK]
Hint: Covariance = output types get more specific [OK]
Common Mistakes:
  • Confusing covariance with contravariance
  • Thinking covariance applies to inputs
  • Believing covariance restricts type changes
2. Which of the following TypeScript function types correctly demonstrates contravariance in the parameter type?
easy
A. (input: Dog) => void
B. (input: Animal) => void
C. (input: string) => void
D. (input: number) => void

Solution

  1. Step 1: Understand contravariance in parameters

    Contravariance means a function can accept a more general type as input than expected.
  2. Step 2: Compare parameter types

    Given Dog extends Animal, accepting Animal (more general) instead of Dog is contravariant.
  3. Final Answer:

    (input: Animal) => void -> Option B
  4. Quick Check:

    Contravariance = input type generalizes [OK]
Hint: Contravariance = input types get more general [OK]
Common Mistakes:
  • Choosing more specific input types for contravariance
  • Mixing input and output type variance
  • Ignoring inheritance relationships
3. Consider the following TypeScript code:
class Animal { name: string = 'animal'; }
class Dog extends Animal { breed: string = 'bulldog'; }

let func1: (dog: Dog) => Dog = (d) => d;
let func2: (animal: Animal) => Animal = func1;

const result = func2(new Animal());
console.log(result.name);

What will be the output when this code runs?
medium
A. animal
B. bulldog
C. undefined
D. Type error at assignment

Solution

  1. Step 1: Analyze function type assignment

    func1 expects a Dog parameter and returns Dog. Assigning func1 to func2 which expects Animal parameter and returns Animal is invalid because parameter types are contravariant and return types covariant.
  2. Step 2: Understand TypeScript type checking

    TypeScript disallows assigning a function expecting a more specific parameter (Dog) to one expecting a more general parameter (Animal) due to contravariance rules.
  3. Final Answer:

    Type error at assignment -> Option D
  4. Quick Check:

    Contravariance blocks incompatible parameter types [OK]
Hint: Check parameter types for contravariance errors [OK]
Common Mistakes:
  • Assuming assignment is allowed without error
  • Ignoring contravariance in function parameters
  • Expecting runtime output instead of compile error
4. The following TypeScript code causes a type error. What is the main cause?
interface Producer<out T> {
  produce: () => T;
}

interface Consumer<in T> {
  consume: (item: T) => void;
}

let producer: Producer<Animal> = { produce: () => new Animal() };
let consumer: Consumer<Dog> = { consume: (d) => console.log(d.breed) };

consumer = producer; // Error here
medium
A. Dog does not extend Animal, so types are incompatible.
B. The produce method is missing a return type.
C. Assigning Producer<Animal> to Consumer<Dog> is invalid due to variance mismatch.
D. The consume method should return a value.

Solution

  1. Step 1: Identify variance roles

    Producer is covariant (outputs), Consumer is contravariant (inputs). Assigning producer to consumer mixes these roles incorrectly.
  2. Step 2: Understand type incompatibility

    Producer<Animal> cannot be assigned to Consumer<Dog> because their variance directions conflict, causing a type error.
  3. Final Answer:

    Assigning Producer<Animal> to Consumer<Dog> is invalid due to variance mismatch. -> Option C
  4. Quick Check:

    Variance mismatch causes assignment error [OK]
Hint: Match variance directions when assigning generic types [OK]
Common Mistakes:
  • Ignoring variance annotations (in/out)
  • Assuming unrelated interfaces can be assigned
  • Confusing input and output roles
5. You want to create a TypeScript function that accepts a callback which can handle any subtype of Animal as input and returns a more specific subtype of Animal. Which function type signature correctly uses covariance and contravariance to allow this flexibility?
hard
A. function processAnimal(callback: (input: Animal) => Dog): Dog
B. function processAnimal(callback: (input: Dog) => Animal): Animal
C. function processAnimal(callback: (input: Animal) => Animal): Dog
D. function processAnimal(callback: (input: Dog) => Dog): Animal

Solution

  1. Step 1: Identify input contravariance

    The callback input should be a general type (Animal) to accept any subtype safely (contravariance).
  2. Step 2: Identify output covariance

    The callback returns a more specific subtype (Dog), allowing covariance in the return type.
  3. Step 3: Match function signature

    function processAnimal(callback: (input: Animal) => Dog): Dog matches these rules: input is Animal (general), output is Dog (specific), and the function returns Dog.
  4. Final Answer:

    function processAnimal(callback: (input: Animal) => Dog): Dog -> Option A
  5. Quick Check:

    Input general, output specific = contravariant input, covariant output [OK]
Hint: Input general, output specific for flexible callbacks [OK]
Common Mistakes:
  • Reversing input and output types
  • Using more specific input types causing errors
  • Ignoring return type covariance
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard