TypeScript Deep Dives
Overview
This skill is the “full manual” companion to typescript-mastery. It focuses on how and why TypeScript’s advanced type system features work, and how to apply them safely in Buzz Stack (Next.js App Router + React 19 + strict TypeScript).
What you should get from this skill:
- •A mental model for mapped + conditional types (including distributive behavior)
- •Practical type-level programming techniques (recursion, computation, template literals)
- •Safer replacements for common
asassertions - •Reusable patterns for API response unwrapping, form validation, and fluent builders
Buzz Stack constraints (important):
- •Strict mode is expected.
- •Avoid
any(preferunknown, generic constraints, and narrowing). - •Prefer discriminated unions and Result-style returns over
undefined/nullambiguity.
Core Concepts
1) Mapped Types: Transforming Object Shapes
Mapped types iterate over keys of an object type to produce a new type.
interface User {
id: string;
name: string;
email: string;
}
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
Key remapping with as
Key remapping changes the output key names (including filtering keys to never).
type GetterMethods<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = GetterMethods<User>;
// { getId: () => string; getName: () => string; getEmail: () => string }
Filtering keys (mapped-type “select”)
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];
type StringKeys<T> = KeysMatching<T, string>;
type UserStringKeys = StringKeys<User>; // "id" | "name" | "email"
Optionality and modifiers (+?, -?, +readonly, -readonly)
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type RequiredProps<T> = {
[K in keyof T]-?: T[K];
};
2) Conditional Types: Branching at the Type Level
A conditional type has the form:
$$ T\ \texttt{extends}\ U\ ?\ X\ :\ Y $$
type IsString<T> = T extends string ? true : false; type A = IsString<string>; // true type B = IsString<number>; // false
Distributive conditionals (the “union splits” rule)
If T is a naked type parameter (appears as T extends ... directly), unions distribute.
type ToArray<T> = T extends unknown ? T[] : never; type X = ToArray<string | number>; // string[] | number[]
To stop distribution, wrap in a tuple.
type ToArrayNonDistributive<T> = [T] extends [unknown] ? T[] : never; type Y = ToArrayNonDistributive<string | number>; // (string | number)[]
3) infer: Extracting Types From Other Types
infer binds a type variable inside a conditional type.
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T; type P1 = UnwrapPromise<Promise<number>>; // number type P2 = UnwrapPromise<string>; // string
Extract function arguments + return types (avoid any[]).
type Fn = (...args: unknown[]) => unknown; type Args<T extends Fn> = T extends (...args: infer A) => unknown ? A : never; type Returns<T extends Fn> = T extends (...args: unknown[]) => infer R ? R : never; type Example = (x: number, y: string) => boolean; type ExampleArgs = Args<Example>; // [number, string] type ExampleReturn = Returns<Example>; // boolean
4) Template Literal Types: Type-Safe Strings
Template literal types let you build string unions that stay tied to types.
type Resource = "user" | "item";
type Action = "created" | "deleted";
type EventName = `${Resource}:${Action}`;
// "user:created" | "user:deleted" | "item:created" | "item:deleted"
Combine with mapped types for strongly-typed event maps.
type EventPayloads = {
"user:created": { userId: string };
"item:deleted": { itemId: string };
};
type EventKey = keyof EventPayloads;
function emit<K extends EventKey>(event: K, payload: EventPayloads[K]) {
console.log(event, payload);
}
emit("user:created", { userId: "u_123" });
emit("user:created", { itemId: "x" }); // Type error
5) Recursive Types: Type-Level Programming
TypeScript supports recursive types with practical limits. Use recursion for:
- •Deep readonly / deep partial
- •Path typing
- •String manipulation
Deep readonly (common in immutable config / computed data)
type Primitive = string | number | boolean | bigint | symbol | null | undefined;
type DeepReadonly<T> = T extends Primitive
? T
: T extends (...args: unknown[]) => unknown
? T
: T extends readonly (infer U)[]
? readonly DeepReadonly<U>[]
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Config {
flags: {
search: { enabled: boolean; mode: "basic" | "advanced" };
};
}
type FrozenConfig = DeepReadonly<Config>;
Deep partial (useful for patch/update payloads)
type DeepPartial<T> = T extends Primitive
? T
: T extends (...args: unknown[]) => unknown
? T
: T extends readonly (infer U)[]
? readonly DeepPartial<U>[]
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
6) Advanced Utility Types (Built-In + Safer Variants)
TypeScript ships utility types; use them deliberately.
interface User {
id: string;
name: string;
email: string;
password: string;
}
type PublicUser = Omit<User, "password">;
type PreviewUser = Pick<User, "id" | "name">;
type Permissions = Record<"read" | "write" | "admin", boolean>;
type UserMaybe = Partial<User>;
type UserAll = Required<UserMaybe>;
Awaited and safe return-type extraction.
type AwaitedLike<T> = T extends Promise<infer U> ? AwaitedLike<U> : T;
type Fetcher = () => Promise<{ data: { id: string } }>;
type FetcherReturn = ReturnType<Fetcher>; // Promise<{ data: { id: string } }>
type FetcherResolved = AwaitedLike<FetcherReturn>; // { data: { id: string } }
ThisParameterType and OmitThisParameter patterns (helpful for binding methods).
function logWithPrefix(this: { prefix: string }, message: string) {
console.log(`${this.prefix}: ${message}`);
}
type LogThis = ThisParameterType<typeof logWithPrefix>; // { prefix: string }
type BoundLog = OmitThisParameter<typeof logWithPrefix>; // (message: string) => void
7) Narrowing: Type Guards, Assertion Functions, and Discriminated Unions
Type-level sophistication is pointless if runtime validation is sloppy. The safe pattern:
- •validate unknown input
- •narrow via type guards / assertions
- •use discriminated unions for state and results
User-defined type guard
interface ApiUser {
id: string;
name: string;
email: string;
}
function isApiUser(value: unknown): value is ApiUser {
if (typeof value !== "object" || value === null) return false;
const record = value as Record<string, unknown>;
return (
typeof record.id === "string" &&
typeof record.name === "string" &&
typeof record.email === "string"
);
}
Assertion function
function assertApiUser(value: unknown): asserts value is ApiUser {
if (!isApiUser(value)) {
throw new Error("Invalid ApiUser shape");
}
}
Discriminated union (compile-time valid states)
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function parseUser(json: unknown): Result<ApiUser, string> {
if (!isApiUser(json)) return { ok: false, error: "Invalid user" };
return { ok: true, value: json };
}
Patterns (Deep, Reusable, Buzz Stack-Friendly)
Pattern 1: API Response Unwrapping With Conditional Types (Buzz Stack Standard)
This pattern appears throughout Buzz Stack docs (see UnwrapApiResponse / UnwrapResponse).
// Works with wrapped OR unwrapped APIs
export type UnwrapApiResponse<T> = T extends { data: infer D } ? D : T;
interface Wrapped<T> {
data: T;
status: number;
}
type Items = UnwrapApiResponse<Wrapped<string[]>>; // string[]
type AlsoItems = UnwrapApiResponse<string[]>; // string[]
Type-safe fetch wrapper that adapts to the response envelope.
export async function fetchAndUnwrap<T>(
url: string,
): Promise<UnwrapApiResponse<T>> {
const response = await fetch(url);
const data: unknown = await response.json();
return data as UnwrapApiResponse<T>;
}
Why this is still safe in practice:
- •you isolate the assertion at the boundary
- •the rest of your app stays assertion-free
Pattern 2: “Key Remap + Getter Generation” for Model Accessors
type GettersFor<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Item {
id: string;
title: string;
matchScore: number;
}
class ItemAccessor implements GettersFor<Item> {
constructor(private readonly item: Item) {}
getId = () => this.item.id;
getTitle = () => this.item.title;
getMatchScore = () => this.item.matchScore;
}
Pattern 3: “Select Keys by Value Type” (e.g., Only Boolean Flags)
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Flags {
enabled: boolean;
mode: "basic" | "advanced";
retries: number;
debug: boolean;
}
type BooleanFlags = PickByValue<Flags, boolean>;
// { enabled: boolean; debug: boolean }
Pattern 4: “Exact Object” for Safer Input Validation
This helps prevent extra keys in config-like objects.
type Exact<TExpected, TActual extends TExpected> =
Exclude<keyof TActual, keyof TExpected> extends never ? TActual : never;
interface LoginPayload {
email: string;
password: string;
}
function acceptLogin<T extends LoginPayload>(payload: Exact<LoginPayload, T>) {
return payload;
}
acceptLogin({ email: "a@b.com", password: "pw" });
acceptLogin({ email: "a@b.com", password: "pw", extra: true }); // Type error
Pattern 5: “Non-Empty Array” for APIs That Require at Least One Item
type NonEmptyArray<T> = readonly [T, ...T[]];
function first<T>(items: NonEmptyArray<T>): T {
return items[0];
}
const ok: NonEmptyArray<number> = [1, 2, 3];
// const bad: NonEmptyArray<number> = []; // Type error
Pattern 6: Form Validation Types: Field Errors + Typed Output
A common shape for validation results:
type FieldErrors<TFields extends string> = Partial<Record<TFields, string>>;
type ValidationResult<TData, TFields extends string> =
| { ok: true; value: TData }
| { ok: false; fieldErrors: FieldErrors<TFields>; formError?: string };
Use it with literal field names.
type SignupFields = "email" | "password" | "confirmPassword";
interface SignupData {
email: string;
password: string;
}
function validateSignup(
form: Record<string, FormDataEntryValue>,
): ValidationResult<SignupData, SignupFields> {
const email = String(form.email ?? "");
const password = String(form.password ?? "");
const confirmPassword = String(form.confirmPassword ?? "");
const fieldErrors: FieldErrors<SignupFields> = {};
if (!email.includes("@")) fieldErrors.email = "Enter a valid email";
if (password.length < 8) fieldErrors.password = "Password must be 8+ chars";
if (password !== confirmPassword)
fieldErrors.confirmPassword = "Passwords must match";
if (Object.keys(fieldErrors).length > 0) {
return { ok: false, fieldErrors };
}
return { ok: true, value: { email, password } };
}
Pattern 7: Builder Pattern With Fluent Chaining (Type-Safe State Progression)
Use conditional types + intersections to build a “must call X before Y” API.
type Has<T, K extends string> = T & Record<K, true>;
type Requires<T, K extends string> = T extends Record<K, true> ? true : false;
interface QueryState {
table?: string;
where?: Record<string, unknown>;
limit?: number;
}
class QueryBuilder<S extends QueryState = {}> {
constructor(private readonly state: S = {} as S) {}
table<TTable extends string>(
table: TTable,
): QueryBuilder<S & { table: TTable }> {
return new QueryBuilder({ ...this.state, table } as S & { table: TTable });
}
where(
where: Record<string, unknown>,
): QueryBuilder<S & { where: Record<string, unknown> }> {
return new QueryBuilder({ ...this.state, where } as S & {
where: Record<string, unknown>;
});
}
limit(limit: number): QueryBuilder<S & { limit: number }> {
return new QueryBuilder({ ...this.state, limit } as S & { limit: number });
}
build(this: QueryBuilder<S & { table: string }>): S {
return this.state;
}
}
const q = new QueryBuilder()
.table("users")
.where({ active: true })
.limit(10)
.build();
// new QueryBuilder().build(); // Type error: table() not called
Pattern 8: “Path Strings” to Access Nested Values Safely
This is helpful for form libs and config access.
type Join<K, P> = K extends string
? P extends string
? `${K}.${P}`
: never
: never;
type Paths<T> = T extends object
? {
[K in keyof T]-?: K extends string
? T[K] extends object
? K | Join<K, Paths<T[K]>>
: K
: never;
}[keyof T]
: never;
interface AppConfig {
flags: {
search: {
enabled: boolean;
mode: "basic" | "advanced";
};
};
}
type ConfigPaths = Paths<AppConfig>;
// "flags" | "flags.search" | "flags.search.enabled" | "flags.search.mode"
Pattern 9: “Narrow Unknown at Boundaries” (JSON, FormData, external APIs)
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function getString(
record: Record<string, unknown>,
key: string,
): string | null {
const v = record[key];
return typeof v === "string" ? v : null;
}
function parseUserFromUnknown(input: unknown): Result<ApiUser, string> {
if (!isRecord(input)) return { ok: false, error: "Not an object" };
const id = getString(input, "id");
const name = getString(input, "name");
const email = getString(input, "email");
if (!id || !name || !email) return { ok: false, error: "Missing fields" };
return { ok: true, value: { id, name, email } };
}
Pattern 10: “Typed Error Codes” for UI-Specific Error Handling
type UpdateUserError =
| {
code: "VALIDATION";
fieldErrors: Partial<Record<"email" | "name", string>>;
}
| { code: "NOT_FOUND" }
| { code: "UNKNOWN"; message: string };
type UpdateUserResult = Result<{ id: string; name: string }, UpdateUserError>;
function renderError(error: UpdateUserError): string {
switch (error.code) {
case "VALIDATION":
return JSON.stringify(error.fieldErrors);
case "NOT_FOUND":
return "User not found";
case "UNKNOWN":
return error.message;
}
}
Pattern 11: “Chaining Result Types” (Railway-Oriented Programming)
type ResultOk<T> = { ok: true; value: T };
type ResultErr<E> = { ok: false; error: E };
type Result2<T, E> = ResultOk<T> | ResultErr<E>;
function mapResult<T, E, U>(
res: Result2<T, E>,
fn: (t: T) => U,
): Result2<U, E> {
return res.ok ? { ok: true, value: fn(res.value) } : res;
}
function andThen<T, E, U>(
res: Result2<T, E>,
fn: (t: T) => Result2<U, E>,
): Result2<U, E> {
return res.ok ? fn(res.value) : res;
}
Pattern 12: Derive Types From Data (Index Access + as const)
const ROUTES = {
home: "/",
search: "/search",
settings: "/settings",
} as const;
type RouteKey = keyof typeof ROUTES; // "home" | "search" | "settings"
type RoutePath = (typeof ROUTES)[RouteKey]; // "/" | "/search" | "/settings"
Anti-Patterns (Avoid These)
Anti-pattern 1: “Sprinkle as Everywhere”
Problem: assertions move uncertainty into runtime and hide broken contracts.
// ❌ Bad: unchecked assertion; any mismatch becomes runtime bug
const json = (await response.json()) as { data: { id: string }[] };
Fix: validate at the boundary, then use narrowed types.
// ✅ Better: validate unknown then narrow
const data: unknown = await response.json();
if (!Array.isArray(data)) throw new Error("Expected array");
Anti-pattern 2: Conditional Types That Accidentally Distribute
// ❌ Distributes over unions unintentionally // string | number => string[] | number[] (maybe you wanted (string|number)[]) type Wrap<T> = T extends unknown ? T[] : never;
Fix: use tuple wrapping to control distribution.
// ✅ Non-distributive // string | number => (string | number)[] type WrapND<T> = [T] extends [unknown] ? T[] : never;
Anti-pattern 3: “Type-Level Sudoku” (Types That Nobody Can Maintain)
If a type requires 5 minutes to understand, it will rot.
Fix: prefer:
- •small composable helpers
- •dedicated docs
- •runtime validation + narrow
Anti-pattern 4: any in Public Types
// ❌ Bad: any erases type safety for consumers export type ApiHandler = (req: any) => any;
Fix: use unknown + narrowing, or generic constraints.
// ✅ Better export type ApiHandler = (req: Request) => Promise<Response>;
Anti-pattern 5: Optional Fields for State (Invalid States)
// ❌ Bad: can represent invalid combinations
interface State<T> {
loading?: boolean;
data?: T;
error?: string;
}
Fix: discriminated union.
// ✅ Better
type State<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
Real-World Buzz Stack Examples (Where These Patterns Show Up)
1) API Response Unwrapping
Buzz Stack docs demonstrate UnwrapApiResponse / UnwrapResponse patterns for handling APIs that sometimes wrap responses as { data: ... }.
- •See:
docs/ARCHITECTURE.mdanddocs/DESIGN_PATTERNS.md
// Same idea as documented in Buzz Stack
export type UnwrapResponse<T> = T extends { data: infer D } ? D : T;
2) Result Types for Error Handling
Buzz Stack docs use Result-like discriminated unions for type-safe error handling (especially in Next.js server actions).
export type Result<T, E = string> =
| { success: true; data: T }
| { success: false; error: E };
3) Derived Types From Central Constants
Using as const + index access keeps your routes/events/config type-safe and refactor-friendly.
const FEATURES = ["search", "voice", "export"] as const; type Feature = (typeof FEATURES)[number]; // "search" | "voice" | "export"
Cross-References
Use these related skills and docs for adjacent guidance:
- •
Skills:
- •
typescript-mastery: ../typescript-mastery/SKILL.md - •
api-design-contracts: ../api-design-contracts/SKILL.md - •
nextjs-app-router-patterns: ../nextjs-app-router-patterns/SKILL.md - •
react-19-patterns: ../react-19-patterns/SKILL.md
- •
- •
Docs:
- •Advanced TS patterns: ../../../docs/ADVANCED-PATTERNS.md
- •Architecture patterns: ../../../docs/ARCHITECTURE.md
- •Design patterns: ../../../docs/DESIGN_PATTERNS.md
- •Best practices: ../../../docs/BEST_PRACTICES.md
Quick Checklist (Before You Ship a Deep Type)
- •Does it avoid
any? (Preferunknown+ narrowing) - •Is distributive behavior intentional?
- •Can you replace a clever type with a clearer one?
- •Is runtime validation present at boundaries?
- •Are consumers forced into handling success/error states?