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
typescripttypescript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667// 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 Validatorclass StringSchema extends Schema<string> {parse(input: unknown): string {if (typeof input !== "string") {throw new Error(`Expected string, received ${typeof input}`);}return input;}}// 3. Number Validatorclass 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-Timetype 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 APIconst z = {string: () => new StringSchema(),number: () => new NumberSchema(),object: <S extends Record<string, Schema<any>>>(shape: S) => new ObjectSchema(shape)};// Define Schemaconst 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 CodeCommon 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 positionCorrect / 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.