Claude-skill-registry advanced-typescript-patterns

Advanced TypeScript patterns for TMNL. Covers conditional types, mapped types, branded types, generic constraints, type inference, and utility type composition. Pure TypeScript patterns beyond Effect Schema.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/advanced-typescript-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-advanced-typescript-patterns && rm -rf "$T"
manifest: skills/data/advanced-typescript-patterns/SKILL.md
source content

Advanced TypeScript Patterns for TMNL

Overview

TMNL uses sophisticated TypeScript patterns that extend beyond Effect Schema. This skill covers:

  1. Conditional Types — Type-level branching with
    infer
  2. Mapped Types — Object transformation with
    [K in keyof]
  3. Branded Types — Nominal typing for type safety
  4. Generic Constraints — Cascading bounds and defaults
  5. Pattern Matching
    Extract<>
    for discriminated unions
  6. Utility Composition — Partial, Readonly, Record, Pick, Omit
  7. Template Literals — String-level type constraints
  8. Type Decomposition — Extract types from complex generics

Scope Distinction: Effect Schema patterns (Schema.TaggedStruct, Schema.brand) are covered in

effect-schema-mastery
. This skill covers pure TypeScript patterns.


Pattern 1: Conditional Types — TYPE-LEVEL BRANCHING

When: Selecting types based on structural conditions.

Basic Conditional Type

// If T extends Array, extract element type; otherwise, return T
type UnwrapArray<T> = T extends Array<infer U> ? U : T

type A = UnwrapArray<string[]>    // string
type B = UnwrapArray<number>      // number

Extracting from Complex Generics

TMNL Example — EffectResult<T> (

src/lib/stx/types.ts:74-78
):

// Extract success/error types from Effect.Effect or Effect-returning function
export type EffectResult<T> = T extends Effect.Effect<infer A, infer E, any>
  ? Result<A, E>
  : T extends (...args: any[]) => Effect.Effect<infer A, infer E, any>
  ? Result<A, E>
  : never

// Usage: Wrap an Effect's types in Result for React consumption
type MyResult = EffectResult<typeof myEffect>

Key insight: Multiple

infer
clauses can extract different type parameters. Chained conditionals handle different input shapes.

Distributed Conditional Types

// T extends U distributes over unions
type ToArray<T> = T extends unknown ? T[] : never

type C = ToArray<string | number>  // string[] | number[] (NOT (string | number)[])

When to use: When you want conditional to apply to each union member separately.

Preventing Distribution

// Wrap in tuple to prevent distribution
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never

type D = ToArrayNonDist<string | number>  // (string | number)[]

Pattern 2: Mapped Types — OBJECT TRANSFORMATION

When: Transforming object properties systematically.

Basic Mapped Type

// Make all properties optional
type Optional<T> = { [K in keyof T]?: T[K] }

// Make all properties readonly
type Frozen<T> = { readonly [K in keyof T]: T[K] }

Heterogeneous Property Transformation

TMNL Example — Effects mapper (

src/lib/stx/types.ts:155-161
):

// Each property transforms based on its source type
readonly effects: {
  [K in keyof TEffects]: TEffects[K] extends (...args: infer Args) => Effect.Effect<infer A, infer E, any>
    ? (...args: Args) => Promise<Result<A, E>>
    : TEffects[K] extends Effect.Effect<infer A, infer E, any>
    ? () => Promise<Result<A, E>>
    : never
}

Breakdown:

  1. [K in keyof TEffects]
    — Iterate over all keys
  2. First conditional: If property is a function returning Effect, preserve args
  3. Second conditional: If property is an Effect value, make it a zero-arg function
  4. never
    fallback: Type error if neither shape matches

Mapped Types with Property Filtering

// Only include string-valued properties
type StringProps<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
}

interface Mixed {
  name: string
  age: number
  email: string
}

type OnlyStrings = StringProps<Mixed>  // { name: string; email: string }

Handler Map Polymorphism

TMNL Example — OverlayHandlerMap (

src/lib/overlays/Overlay.ts:91-93
):

export type OverlayEventTag = 'Open' | 'Close' | 'Toggle' | 'Focus'

export type TypedEventHandler<T extends OverlayEventTag> = (
  event: Extract<OverlayEvent, { _tag: T }>,
  context: OverlayHandlerContext
) => Effect.Effect<HandlerResult>

// Each key maps to a handler for that specific event type
export type OverlayHandlerMap = {
  [K in OverlayEventTag]?: TypedEventHandler<K>
}

Pattern 3: Branded Types — NOMINAL TYPING

When: Preventing confusion between structurally identical types.

The Problem

type UserId = string
type PostId = string

function getPost(postId: PostId): Post { ... }

const userId: UserId = 'user-123'
getPost(userId)  // Compiles! But semantically wrong

The Solution: Branded Types

// Unique symbol for brand discrimination
declare const UserIdBrand: unique symbol
declare const PostIdBrand: unique symbol

type UserId = string & { readonly [UserIdBrand]: typeof UserIdBrand }
type PostId = string & { readonly [PostIdBrand]: typeof PostIdBrand }

// Now this is a compile error:
const userId = 'user-123' as UserId
getPost(userId)  // Error: UserId is not assignable to PostId

Generic Brand Factory

TMNL Example — Token<N> (

src/lib/primitives/TokenRegistry/types.ts:26-38
):

import { Brand } from 'effect'

// Generic brand factory creates distinct types per namespace
export type Token<N extends string> = string & Brand.Brand<N>

export const TokenSchema = <N extends string>(namespace: N) =>
  Schema.String.pipe(
    Schema.nonEmptyString(),
    Schema.brand(namespace)
  )

// Usage:
type ColorToken = Token<'color'>
type SpacingToken = Token<'spacing'>

const color: ColorToken = 'red-500' as ColorToken
const spacing: SpacingToken = '4' as SpacingToken

// Error: ColorToken not assignable to SpacingToken
const wrong: SpacingToken = color

Registry Enforcement

interface TokenRegistry<N extends string> {
  readonly get: (key: string) => Token<N> | undefined
  readonly set: (key: string, value: Token<N>) => void
}

const colorRegistry: TokenRegistry<'color'> = /* ... */
const spacingRegistry: TokenRegistry<'spacing'> = /* ... */

// Type system prevents cross-registry operations
colorRegistry.set('primary', spacingRegistry.get('4')!)  // Error!

Pattern 4: Generic Constraints — CASCADING BOUNDS

When: Building flexible APIs with type-safe defaults.

Basic Constraint

interface Indexable {
  id: string
}

interface IndexConfig<T extends Indexable> {
  readonly fields: readonly (keyof T)[]
  readonly idField?: keyof T
}

// Usage:
interface User extends Indexable { name: string; email: string }
const config: IndexConfig<User> = {
  fields: ['name', 'email'],  // Autocomplete works!
  idField: 'id'
}

Multi-Parameter Constraints with Cascading

TMNL Example — StxConfig (

src/lib/stx/types.ts:34-50
):

export interface StxConfig<
  TMachine extends AnyStateMachine | undefined = undefined,
  TData extends object = object,
  TEffects extends EffectsConfig = EffectsConfig,
  TComputed extends ComputedConfig<TData, TMachine> = ComputedConfig<TData, TMachine>,
> {
  readonly machine?: TMachine
  readonly data?: TData
  readonly effects?: TEffects
  readonly computed?: TComputed
  readonly bindings?: BindingsConfig<TData, TMachine>
}

Key patterns:

  1. = undefined
    default allows optional generics
  2. Later params can reference earlier ones (
    TComputed
    uses
    TData
    ,
    TMachine
    )
  3. Constraints flow through composition

Conditional Property Shape

export interface StxGetter<TData extends object, TMachine extends AnyStateMachine | undefined> {
  readonly data: ObservableObject<TData>
  // Property shape depends on generic parameter
  readonly machine: TMachine extends AnyStateMachine
    ? {
        matches: (state: string) => boolean
        snapshot: SnapshotFrom<TMachine>
        context: SnapshotFrom<TMachine>['context']
      }
    : undefined
}

Pattern 5: Extract/Exclude — DISCRIMINATED UNION MATCHING

When: Type-safe operations on union members.

Extract Basic Usage

type Event =
  | { type: 'click'; x: number; y: number }
  | { type: 'keypress'; key: string }
  | { type: 'scroll'; delta: number }

// Extract members matching a condition
type ClickEvent = Extract<Event, { type: 'click' }>
// { type: 'click'; x: number; y: number }

type InputEvents = Extract<Event, { type: 'click' } | { type: 'keypress' }>
// ClickEvent | KeypressEvent

Exclude Usage

// Remove members matching a condition
type NonClickEvents = Exclude<Event, { type: 'click' }>
// { type: 'keypress'; ... } | { type: 'scroll'; ... }

Type-Safe Event Dispatch

TMNL Example — TypedEventHandler (

src/lib/overlays/Overlay.ts:85-88
):

type OverlayEventTag = 'Open' | 'Close' | 'Toggle' | 'Focus'

type OverlayEvent =
  | { _tag: 'Open'; overlayId: string }
  | { _tag: 'Close'; overlayId: string }
  | { _tag: 'Toggle'; overlayId: string }
  | { _tag: 'Focus'; overlayId: string; element: HTMLElement }

// Handler knows exact event shape based on T
type TypedEventHandler<T extends OverlayEventTag> = (
  event: Extract<OverlayEvent, { _tag: T }>,  // Narrow to specific event
  context: OverlayHandlerContext
) => Effect.Effect<HandlerResult>

// Usage:
const openHandler: TypedEventHandler<'Open'> = (event, ctx) => {
  // event is narrowed to { _tag: 'Open'; overlayId: string }
  console.log(event.overlayId)  // Works!
  // event.element  // Error: 'element' doesn't exist on Open event
}

Pattern 6: Utility Type Composition

When: Building immutable, constrained object types.

Composition Patterns

// Read-only record with string keys
type ImmutableConfig = Readonly<Record<string, number>>

// Partial version of a record
type OptionalConfig = Partial<Record<'a' | 'b' | 'c', number>>

// Pick specific properties
type Subset = Pick<FullConfig, 'name' | 'version'>

// Omit specific properties
type WithoutSecret = Omit<User, 'password' | 'ssn'>

Component Registry Pattern

TMNL Example — CapabilityMap (

src/lib/capabilities/types.ts:140-145
):

export interface CapabilityMap {
  position: PositionCapability
  velocity: VelocityCapability
  collision: CollisionCapability
  render: RenderCapability
}

// Indexed access type
export type Component<K extends CapabilityName> = CapabilityMap[K]

// Partial for optional components
export type EntityComponents = Partial<CapabilityMap>

// Usage:
const entity: EntityComponents = {
  position: { x: 0, y: 0 },
  // velocity optional
  render: { visible: true }
}

Deep Readonly

type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T

interface Config {
  db: { host: string; port: number }
  cache: { ttl: number }
}

type FrozenConfig = DeepReadonly<Config>
// All nested properties are readonly

Pattern 7: Template Literal Types

When: Constraining string values at the type level.

Basic Template Literal

type ColorHex = `#${string}`

const valid: ColorHex = '#ff0000'  // OK
const invalid: ColorHex = 'red'     // Error: Type '"red"' is not assignable

TMNL Example — ColorValue (

src/lib/animation/v2/types.ts:22
):

export type ColorValue = `#${string}`

interface AnimationConfig {
  color?: ColorValue
}

Constrained Template Literals

// Hex digits only
type HexDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
  | 'a' | 'b' | 'c' | 'd' | 'e' | 'f'
  | 'A' | 'B' | 'C' | 'D' | 'E' | 'F'

// Two-char hex
type Hex2 = `${HexDigit}${HexDigit}`

// Full hex color
type HexColor = `#${Hex2}${Hex2}${Hex2}`

Event Name Patterns

type DomEventName = `on${Capitalize<keyof HTMLElementEventMap>}`
// 'onClick' | 'onMousedown' | 'onKeypress' | ...

type Handler<T extends DomEventName> = T extends `on${infer E}`
  ? (event: HTMLElementEventMap[Uncapitalize<E>]) => void
  : never

Pattern 8: Type Decomposition — EXTRACTING FROM GENERICS

When: Pulling apart complex composed types.

Basic Decomposition

interface Stx<TMachine, TData, TEffects, TComputed> {
  readonly machine: TMachine
  readonly data: TData
  readonly effects: TEffects
  readonly computed: TComputed
}

// Extract each type parameter
type DataOf<S> = S extends Stx<any, infer D, any, any> ? D : never
type MachineOf<S> = S extends Stx<infer M, any, any, any> ? M : never
type EffectsOf<S> = S extends Stx<any, any, infer E, any> ? E : never
type ComputedOf<S> = S extends Stx<any, any, any, infer C> ? C : never

TMNL Example (

src/lib/stx/types.ts:262-278
):

// Usage in components
const stx = createStx({ data: { count: 0 }, effects: { save: Effect.void } })

type MyData = DataOf<typeof stx>        // { count: number }
type MyEffects = EffectsOf<typeof stx>  // { save: Effect.Effect<void> }

Promise Unwrapping

type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T

type A = Awaited<Promise<string>>              // string
type B = Awaited<Promise<Promise<number>>>     // number (recursive)

Array Element Extraction

type ElementOf<T> = T extends ReadonlyArray<infer U> ? U : never

type Items = ElementOf<typeof ['a', 'b', 'c']>  // 'a' | 'b' | 'c'

Anti-Patterns

1. Overusing
any
in Conditionals

// WRONG — any leaks type safety
type Bad<T> = T extends any ? T[] : never

// CORRECT — Use unknown for truly unconstrained
type Good<T> = T extends unknown ? T[] : never

2. Forgetting Distribution Behavior

// WRONG — Expects single output, gets union
type Confused<T> = T extends string ? 'string' : 'other'
type X = Confused<string | number>  // 'string' | 'other', NOT 'other'

// CORRECT — Prevent distribution if needed
type Fixed<T> = [T] extends [string] ? 'string' : 'other'
type Y = Fixed<string | number>  // 'other'

3. Circular Type References

// WRONG — Infinite recursion
type Broken<T> = { value: Broken<T> }  // Error: Type alias circularly references itself

// CORRECT — Use interface for self-reference
interface Node<T> {
  value: T
  children: Node<T>[]  // Allowed with interface
}

4. Overly Complex Conditional Chains

// WRONG — Unreadable, unmaintainable
type Monster<T> = T extends A ? (T extends B ? (T extends C ? D : E) : F) : G

// CORRECT — Break into named helper types
type IsA<T> = T extends A ? true : false
type IsB<T> = T extends B ? true : false
type Resolve<T, A extends boolean, B extends boolean> = ...

Decision Tree: Which Pattern to Use

Need to transform object properties?
│
├─ Same transformation for all props?
│  └─ Use: Mapped type { [K in keyof T]: Transform<T[K]> }
│
├─ Different transformation per prop?
│  └─ Use: Mapped type with conditional { [K in keyof T]: T[K] extends X ? A : B }
│
├─ Filter/remove some props?
│  └─ Use: as clause { [K in keyof T as Filter<K>]: T[K] }
│
Need to extract types from generics?
│
├─ From Effect/Promise/Array?
│  └─ Use: Conditional with infer (T extends X<infer U> ? U : never)
│
├─ From discriminated union?
│  └─ Use: Extract<Union, { _tag: 'Specific' }>
│
├─ Remove from discriminated union?
│  └─ Use: Exclude<Union, { _tag: 'Remove' }>
│
Need type safety between same structures?
│
└─ Use: Branded types (Brand.Brand<'Name'>)

File Locations Summary

PatternFileLinesDescription
EffectResult conditional
src/lib/stx/types.ts
74-78Extract from Effect types
Effects heterogeneous map
src/lib/stx/types.ts
155-161Mapped type with conditional
StxConfig cascading
src/lib/stx/types.ts
34-50Multi-param constraints
TypedEventHandler
src/lib/overlays/Overlay.ts
85-88Extract for type-safe dispatch
Token<N> brand factory
src/lib/primitives/TokenRegistry/types.ts
26-38Generic branded types
ColorValue template
src/lib/animation/v2/types.ts
22Template literal
CapabilityMap
src/lib/capabilities/types.ts
140-145Utility composition
IndexConfig<T>
src/lib/search/types.ts
100-107Constrained generic

Integration Points

  • effect-schema-mastery — When patterns involve Schema.TaggedStruct, Schema.brand
  • effect-patterns — When patterns define Effect.Service generic shapes
  • common-conventions — Naming and file organization for type definitions
  • tmnl-registry-patterns — Generic registry types with branded keys