AgentSkillsCN

typescript-deep-dives

深入探索映射类型、条件类型、类型级别编程、高级实用工具类型,以及 Buzz Stack 的收窄模式。

SKILL.md
--- frontmatter
name: typescript-deep-dives
description: Exhaustive deep dives into mapped types, conditional types, type-level programming, advanced utility types, and narrowing patterns for Buzz Stack.
argument-hint: Describe the TypeScript deep-dive you need (mapped types, infer, conditional transforms, builders, validation types, API unwrapping, narrowing).

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 as assertions
  • Reusable patterns for API response unwrapping, form validation, and fluent builders

Buzz Stack constraints (important):

  • Strict mode is expected.
  • Avoid any (prefer unknown, generic constraints, and narrowing).
  • Prefer discriminated unions and Result-style returns over undefined/null ambiguity.

Core Concepts

1) Mapped Types: Transforming Object Shapes

Mapped types iterate over keys of an object type to produce a new type.

typescript
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).

typescript
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”)

typescript
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)

typescript
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 $$

typescript
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.

typescript
type ToArray<T> = T extends unknown ? T[] : never;

type X = ToArray<string | number>; // string[] | number[]

To stop distribution, wrap in a tuple.

typescript
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.

typescript
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[]).

typescript
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.

typescript
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.

typescript
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)

typescript
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)

typescript
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.

typescript
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.

typescript
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).

typescript
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

typescript
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

typescript
function assertApiUser(value: unknown): asserts value is ApiUser {
  if (!isApiUser(value)) {
    throw new Error("Invalid ApiUser shape");
  }
}

Discriminated union (compile-time valid states)

typescript
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).

typescript
// 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.

typescript
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

typescript
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)

typescript
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.

typescript
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

typescript
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:

typescript
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.

typescript
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.

typescript
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.

typescript
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)

typescript
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

typescript
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)

typescript
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)

typescript
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.

typescript
// ❌ 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.

typescript
// ✅ 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

typescript
// ❌ 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.

typescript
// ✅ 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

typescript
// ❌ Bad: any erases type safety for consumers
export type ApiHandler = (req: any) => any;

Fix: use unknown + narrowing, or generic constraints.

typescript
// ✅ Better
export type ApiHandler = (req: Request) => Promise<Response>;

Anti-pattern 5: Optional Fields for State (Invalid States)

typescript
// ❌ Bad: can represent invalid combinations
interface State<T> {
  loading?: boolean;
  data?: T;
  error?: string;
}

Fix: discriminated union.

typescript
// ✅ 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.md and docs/DESIGN_PATTERNS.md
typescript
// 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).

typescript
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.

typescript
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? (Prefer unknown + 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?