💡 K extends keyof T constrains K to be one of the actual keys of T. This makes getProperty(user, "name") safe — TypeScript knows the return type is string, not unknown. Without this constraint, you'd need to use as unknown casts everywhere.
📸 Verified Output:
Step 2: Conditional Types & infer
💡 infer names a type variable within a conditional type.T extends Promise<infer V> says "if T is a Promise of something, name that something V and use it." This is the only way to extract type components from generic types. ReturnType, Parameters, InstanceType all use infer.
📸 Verified Output:
Step 3: Generic Data Structures
💡 Optional<T> (Java-style Maybe monad) avoids null pointer errors — instead of returning null, return Optional.empty(). Callers use map, filter, and getOrElse to chain operations safely. If the optional is empty, operations are skipped automatically.
💡 Generic caches (TTLCache<K, V>) are reusable for any key/value types — TTLCache<string, User> or TTLCache<number, Product>. The type parameters propagate through all methods, so get() returns V | undefined and TypeScript knows the exact type at each call site.
📸 Verified Output:
Summary
Generics are TypeScript's superpower. You've covered constrained generics, infer for type extraction, Optional<T>, Result<T,E>, generic builders, repositories, middleware pipelines, typed event emitters, and a TTL cache. These patterns appear in every production TypeScript codebase.
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type PromiseValue<T> = T extends Promise<infer V> ? V : T;
type FunctionReturn<T> = T extends (...args: unknown[]) => infer R ? R : never;
type FunctionParams<T> = T extends (...args: infer P) => unknown ? P : never;
// Deeply unwrap arrays
type DeepFlatten<T> = T extends (infer E)[] ? DeepFlatten<E> : T;
type E1 = ElementOf<string[]>; // string
type E2 = ElementOf<number[][]>; // number[] (one level)
type E3 = DeepFlatten<number[][][]>; // number
type P1 = PromiseValue<Promise<string>>; // string
type P2 = PromiseValue<number>; // number (not a promise)
// Awaited<T> — recursively unwrap promises
type MyAwaited<T> = T extends Promise<infer V> ? MyAwaited<V> : T;
type A1 = MyAwaited<Promise<Promise<string>>>; // string
// Practical: get the type a function returns
async function fetchUser() { return { id: 1, name: "Dr. Chen" }; }
type User = Awaited<ReturnType<typeof fetchUser>>; // { id: number; name: string }
// Distributive conditional types
type StringOrNumber<T> = T extends string ? "it's a string" : "it's not a string";
type R1 = StringOrNumber<string | number>; // "it's a string" | "it's not a string"
console.log("Conditional types checked at compile time");
console.log("infer keyword extracts type from within another type");
// Runtime usage
function isArray<T>(val: T | T[]): val is T[] {
return Array.isArray(val);
}
const maybeArr: string | string[] = ["a", "b", "c"];
if (isArray(maybeArr)) {
console.log("Is array:", maybeArr.length);
}
Conditional types checked at compile time
infer keyword extracts type from within another type
Is array: 3
class Optional<T> {
private constructor(private readonly _value: T | null) {}
static of<T>(value: T): Optional<T> { return new Optional(value); }
static empty<T>(): Optional<T> { return new Optional<T>(null); }
static from<T>(val: T | null | undefined): Optional<T> {
return val == null ? Optional.empty() : Optional.of(val);
}
isPresent(): boolean { return this._value !== null; }
get(): T {
if (!this.isPresent()) throw new Error("Optional is empty");
return this._value as T;
}
getOrElse(defaultVal: T): T { return this.isPresent() ? (this._value as T) : defaultVal; }
map<U>(fn: (val: T) => U): Optional<U> { return this.isPresent() ? Optional.of(fn(this._value as T)) : Optional.empty(); }
filter(pred: (val: T) => boolean): Optional<T> { return this.isPresent() && pred(this._value as T) ? this : Optional.empty(); }
flatMap<U>(fn: (val: T) => Optional<U>): Optional<U> { return this.isPresent() ? fn(this._value as T) : Optional.empty(); }
}
const users = [
{ id: 1, name: "Dr. Chen", email: "[email protected]" },
{ id: 2, name: "Alice", email: null },
];
users.forEach(u => {
const email = Optional.from(u.email)
.map(e => e.toLowerCase())
.filter(e => e.includes("@"))
.getOrElse("[email protected]");
console.log(`${u.name}: ${email}`);
});
// Generic Result type
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T, never> { return { ok: true, value }; }
function err<E extends Error>(error: E): Result<never, E> { return { ok: false, error }; }
function divideResult(a: number, b: number): Result<number> {
return b === 0 ? err(new Error("Division by zero")) : ok(a / b);
}
[divideResult(10, 2), divideResult(10, 0)].forEach(r => {
console.log(r.ok ? `Result: ${r.value}` : `Error: ${r.error.message}`);
});