Covariance and contravariance in Typescript - Time & Space Complexity
Let's explore how the time cost changes when using covariance and contravariance in TypeScript functions.
We want to see how the number of operations grows as input types vary.
Analyze the time complexity of the following TypeScript function assignments.
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
let dogFunc: (dog: Dog) => Dog = (dog) => { dog.bark(); return dog; };
let animalFunc: (animal: Animal) => Animal;
// Assigning animalFunc to dogFunc (contravariance in parameter, covariance in return)
animalFunc = dogFunc;
// Using animalFunc
const result = animalFunc({ name: 'Buddy' });
This code shows function type assignments demonstrating covariance and contravariance.
Look for operations that repeat or scale with input size.
- Primary operation: Function call and object property access.
- How many times: Each call runs once per invocation; no loops or recursion here.
Since the function runs once per call, the time grows directly with how many times you call it.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | 10 function calls |
| 100 | 100 function calls |
| 1000 | 1000 function calls |
Pattern observation: The time grows linearly with the number of calls, not with type complexity.
Time Complexity: O(n)
This means the time grows in a straight line with how many times the function is called.
[X] Wrong: "Covariance and contravariance change how fast the code runs."
[OK] Correct: These concepts affect type safety and assignment rules, not how many steps the code takes to run.
Understanding covariance and contravariance helps you reason about safe code changes and function assignments, a skill that shows clear thinking about types and behavior.
What if the function accepted an array of Dogs instead of a single Dog? How would the time complexity change?
Practice
covariance in TypeScript?Solution
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.Step 2: Compare with contravariance
Contravariance is about inputs and generalizing types, so it is different from covariance.Final Answer:
It allows replacing a type with a more specific type safely, mainly in outputs. -> Option AQuick Check:
Covariance = more specific output type [OK]
- Confusing covariance with contravariance
- Thinking covariance applies to inputs
- Believing covariance restricts type changes
Solution
Step 1: Understand contravariance in parameters
Contravariance means a function can accept a more general type as input than expected.Step 2: Compare parameter types
Given Dog extends Animal, accepting Animal (more general) instead of Dog is contravariant.Final Answer:
(input: Animal) => void -> Option BQuick Check:
Contravariance = input type generalizes [OK]
- Choosing more specific input types for contravariance
- Mixing input and output type variance
- Ignoring inheritance relationships
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?
Solution
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.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.Final Answer:
Type error at assignment -> Option DQuick Check:
Contravariance blocks incompatible parameter types [OK]
- Assuming assignment is allowed without error
- Ignoring contravariance in function parameters
- Expecting runtime output instead of compile error
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 hereSolution
Step 1: Identify variance roles
Producer is covariant (outputs), Consumer is contravariant (inputs). Assigning producer to consumer mixes these roles incorrectly.Step 2: Understand type incompatibility
Producer<Animal> cannot be assigned to Consumer<Dog> because their variance directions conflict, causing a type error.Final Answer:
Assigning Producer<Animal> to Consumer<Dog> is invalid due to variance mismatch. -> Option CQuick Check:
Variance mismatch causes assignment error [OK]
- Ignoring variance annotations (in/out)
- Assuming unrelated interfaces can be assigned
- Confusing input and output roles
Animal as input and returns a more specific subtype of Animal. Which function type signature correctly uses covariance and contravariance to allow this flexibility?Solution
Step 1: Identify input contravariance
The callback input should be a general type (Animal) to accept any subtype safely (contravariance).Step 2: Identify output covariance
The callback returns a more specific subtype (Dog), allowing covariance in the return type.Step 3: Match function signature
function processAnimal(callback: (input: Animal) => Dog): Dogmatches these rules: input is Animal (general), output is Dog (specific), and the function returns Dog.Final Answer:
function processAnimal(callback: (input: Animal) => Dog): Dog -> Option AQuick Check:
Input general, output specific = contravariant input, covariant output [OK]
- Reversing input and output types
- Using more specific input types causing errors
- Ignoring return type covariance
Ready to prove it?
Pass the test and this pattern moves unknown
