Lab 03: Conditional & Mapped Types

Time: 40 minutes | Level: Advanced | Docker: docker run -it --rm node:20-alpine sh

Master TypeScript's most powerful type-level utilities by implementing them from scratch: DeepPartial, DeepReadonly, DeepRequired, Flatten, UnionToIntersection, OmitNever, and PickByValue.


Step 1: Environment Setup

docker run -it --rm node:20-alpine sh
apk add --no-cache curl
npm install -g typescript ts-node
mkdir lab03 && cd lab03
echo '{"compilerOptions":{"module":"commonjs","target":"es2020","strict":true}}' > tsconfig.json

💡 We use strict: true to catch type errors. All our utilities must satisfy the strictest TypeScript checks.


Step 2: DeepPartial and DeepReadonly

These recursively modify object properties:

// types.ts
type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T;

type DeepReadonly<T> = T extends object
  ? { readonly [P in keyof T]: DeepReadonly<T[P]> }
  : T;

interface Config {
  host: string;
  port: number;
  options: {
    timeout: number;
    retries: number;
    ssl: { cert: string; key: string };
  };
}

// DeepPartial: all nested props optional
const partial: DeepPartial<Config> = {
  host: 'localhost',
  options: { timeout: 5000 }, // ssl and retries optional too!
};

// DeepReadonly: all nested props readonly
const frozen: DeepReadonly<Config> = {
  host: 'prod.server.com',
  port: 443,
  options: { timeout: 30000, retries: 3, ssl: { cert: 'cert.pem', key: 'key.pem' } },
};
// frozen.host = 'other'; // Error: Cannot assign to 'host' because it is a read-only property

💡 The conditional T extends object stops recursion at primitives like string, number, boolean.


Step 3: DeepRequired

Remove all optional modifiers recursively:

💡 The -? modifier is the inverse of ?. Similarly, -readonly removes readonly. These are "mapping modifiers."


Step 4: Flatten

Extract the element type from an array:


Step 5: UnionToIntersection

Convert a union type to an intersection — one of the trickiest type-level operations:

💡 Contravariance trick: Function parameter types are contravariant, meaning (x: A) => void is a subtype of (x: A | B) => void. TypeScript leverages this for intersection inference.


Step 6: OmitNever and PickByValue

Filter object properties by type:

💡 The as clause in mapped types (TypeScript 4.1+) remaps or filters keys. Using as never removes the key entirely.


Step 7: Combining Utilities

Build a practical type-safe configuration system:


Step 8: Capstone — Type Utility Library

Build a complete type utility module with tests:

Run it:

📸 Verified Output:


Summary

Utility
Pattern
Use Case

DeepPartial<T>

Recursive ? on all keys

Config overrides

DeepReadonly<T>

Recursive readonly on all keys

Immutable state

DeepRequired<T>

Recursive -? on all keys

Form completion

Flatten<T>

infer U from array

Unwrap array elements

UnionToIntersection<U>

Contravariant function inference

Merge mixins

OmitNever<T>

as never key remapping

Filter conditional types

PickByValue<T,V>

extends V key remapping

Select fields by type

Last updated