Advanced TypeScript patterns for TMNL. Covers conditional types, mapped types, branded types, generic constraints, type inference, and utility type composition...
TMNL uses sophisticated TypeScript patterns that extend beyond Effect Schema. This skill covers:
infer[K in keyof]Extract<> for discriminated unionsScope Distinction: Effect Schema patterns (Schema.TaggedStruct, Schema.brand) are covered in effect-schema-mastery. This skill covers pure TypeScript patterns.
When: Selecting types based on structural conditions.
// 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
TMNL Example β EffectResultsrc/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.
// 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.
// Wrap in tuple to prevent distribution
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never
type D = ToArrayNonDist<string | number> // (string | number)[]
When: Transforming object properties systematically.
// 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] }
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:
[K in keyof TEffects] β Iterate over all keysnever fallback: Type error if neither shape matches// 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 }
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>
}
When: Preventing confusion between structurally identical types.
type UserId = string
type PostId = string
function getPost(postId: PostId): Post { ... }
const userId: UserId = 'user-123'
getPost(userId) // Compiles! But semantically wrong
// 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
TMNL Example β Tokensrc/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
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!
When: Building flexible APIs with type-safe defaults.
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'
}
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:
= undefined default allows optional genericsTComputed uses TData, TMachine)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
}
When: Type-safe operations on union members.
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
// Remove members matching a condition
type NonClickEvents = Exclude<Event, { type: 'click' }>
// { type: 'keypress'; ... } | { type: 'scroll'; ... }
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
}
When: Building immutable, constrained object types.
// 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'>
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 }
}
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
When: Constraining string values at the type level.
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
}
// 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}`
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
When: Pulling apart complex composed types.
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> }
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)
type ElementOf<T> = T extends ReadonlyArray<infer U> ? U : never
type Items = ElementOf<typeof ['a', 'b', 'c']> // 'a' | 'b' | 'c'
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
// 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'
// 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
}
// 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> = ...
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'>)
| Pattern | File | Lines | Description |
|---|---|---|---|
| EffectResult conditional | src/lib/stx/types.ts |
74-78 | Extract from Effect types |
| Effects heterogeneous map | src/lib/stx/types.ts |
155-161 | Mapped type with conditional |
| StxConfig cascading | src/lib/stx/types.ts |
34-50 | Multi-param constraints |
| TypedEventHandler | src/lib/overlays/Overlay.ts |
85-88 | Extract for type-safe dispatch |
| Token |
src/lib/primitives/TokenRegistry/types.ts |
26-38 | Generic branded types |
| ColorValue template | src/lib/animation/v2/types.ts |
22 | Template literal |
| CapabilityMap | src/lib/capabilities/types.ts |
140-145 | Utility composition |
| IndexConfig |
src/lib/search/types.ts |
100-107 | Constrained generic |