QUICK START:HTMLCSSJavaScriptTypeScriptPythonSQLReactNext.jsNode.jsLinux & UbuntuKotlinSwiftC# / .NETJavaGoRustC++DSASystem DesignDevOpsCybersecurityAI / ML
Advanced 24 min readModule: Module 16: Variance Annotations (`in out`) & Type-Safe Schemas

Variance Annotations (in/out) & Schema Validation

Master type variance: Covariance, Contravariance, Bivariance, and Invariance, explicit `in out` variance annotations in TypeScript 4.7+, and building a type-safe runtime schema engine.

What You Will Learn in This Lesson

  • The 4 forms of Variance: Covariance (returns), Contravariance (arguments), Invariance, and Bivariance
  • Explicit variance annotations: `type Getter<out T>` (Covariant) and `type Setter<in T>` (Contravariant)
  • How variance annotations speed up TypeScript type-checking performance by up to 50%
  • Building a lightweight, zero-dependency type-safe schema validator (like micro-Zod)

Introduction & Core Concept

Variance describes how subtyping between more complex types relates to subtyping between their components. For example, if 'Cat' is a subtype of 'Animal', is 'Getter<Cat>' a subtype of 'Getter<Animal>'? (Covariant). Is 'Setter<Cat>' a subtype of 'Setter<Animal>'? (Contravariant). TypeScript 4.7 introduced explicit 'in' and 'out' variance annotations, allowing developers to explicitly declare type relationships and dramatically accelerate compiler performance.
WHY DOES THIS MATTER IN THE REAL WORLD?

Without explicit variance annotations, TypeScript must recursively compare the entire structural shape of deeply nested generic types. Explicit 'in out' annotations tell the compiler the variance contract immediately, cutting type-checking times in large codebases in half.

Syntax & Structure

typescript
interface Producer<out T> { get(): T; }
interface Consumer<in T> { set(value: T): void; }
interface Invariant<in out T> { get(): T; set(val: T): void; }

Building a Type-Safe Runtime Schema Engine with Type Inference

typescript
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// Type-Safe Runtime Schema Engine with Inferred Types
// 1. Abstract Schema with Covariant Output Type Parameter (out T)
abstract class Schema<out T> {
abstract parse(input: unknown): T;
}
// 2. String Validator
class StringSchema extends Schema<string> {
parse(input: unknown): string {
if (typeof input !== "string") {
throw new Error(`Expected string, received ${typeof input}`);
}
return input;
}
}
// 3. Number Validator
class NumberSchema extends Schema<number> {
parse(input: unknown): number {
if (typeof input !== "number" || isNaN(input)) {
throw new Error(`Expected number, received ${typeof input}`);
}
return input;
}
}
// 4. Object Validator Inferring Shape at Compile-Time
type Infer<S> = S extends Schema<infer T> ? T : never;
class ObjectSchema<Shape extends Record<string, Schema<any>>> extends Schema<{
[K in keyof Shape]: Infer<Shape[K]>
}> {
constructor(private shape: Shape) {
super();
}
parse(input: unknown): { [K in keyof Shape]: Infer<Shape[K]> } {
if (typeof input !== "object" || input === null) {
throw new Error("Expected object.");
}
const result: any = {};
for (const key in this.shape) {
result[key] = this.shape[key].parse((input as any)[key]);
}
return result;
}
}
// Fluent API
const z = {
string: () => new StringSchema(),
number: () => new NumberSchema(),
object: <S extends Record<string, Schema<any>>>(shape: S) => new ObjectSchema(shape)
};
// Define Schema
const UserSchema = z.object({
id: z.string(),
score: z.number()
});
// Inferred TypeScript Type: { id: string; score: number }
type User = Infer<typeof UserSchema>;
const validData = UserSchema.parse({ id: "usr_99", score: 98.5 });
console.log("✅ Verified Valid Object at Runtime:", validData);

Line-by-Line Technical Breakdown

1Why Function Arguments are Contravariant: If a function expects a `Cat` handler, you can safely pass an `Animal` handler (it handles everything Cat does and more). But you cannot pass a `SiameseCat` handler (it might fail on other Cats). Thus, function parameter types invert the subtyping hierarchy (`in T`).

Try It Yourself (Interactive Editor)

Modify the code in real-time and click Run to test live browser output and console logs.

Intelligent Code Runner & Live Sandbox[TYPESCRIPT]
TYPESCRIPT SOURCE EDITOR
Interactive Live Code

Common Mistakes & How to Avoid Them

#1: Marking a type parameter as `out T` when it appears in input/argument positions.

`out` is only valid when T appears exclusively in return/output positions. `in` is required when T appears in parameter positions.

Incorrect / Antipattern
interface Bad<out T> { consume(x: T): void; } // Compiler Error: Contravariant position
Correct / Professional Solution
interface Good<in T> { consume(x: T): void; }

Industry Best Practices & Professional Standards

  • Use explicit `out T` for read-only stores, promises, and collections.
  • Use explicit `in T` for event listeners, serializers, and comparison functions.
  • Build type-safe schema validators to eliminate `any` casts at network boundaries.

Lesson Summary & Core Takeaways

  • Variance annotations (`in`, `out`) declare structural subtyping behavior explicitly.
  • Covariant (`out T`) types produce values; Contravariant (`in T`) types consume values.
  • Explicit variance annotations significantly improve compile-time type-checking speed.