Scope Functions

Stop drowning in temporary variables and nested callbacks. Transform verbose spaghetti code into elegant, readable pipelines.

The Problem: TypeScript's Variable Hell

Every TypeScript developer knows this pain. You've been there.

The Familiar Nightmare

// We've all written this mess...
const data = await fetchData();
const validated = validateData(data);
console.log("Validation complete:", validated);
const transformed = transformData(validated);
const enriched = enrichWithMetadata(transformed);
console.log("Processing:", enriched.id);
const compressed = compressData(enriched);
const cached = await cacheData(compressed);
const result = formatForDisplay(cached);
return result;

// Variables everywhere. No clear flow. Cognitive overload.
// Each line depends on remembering what came before.
// Refactoring? Good luck moving these lines around.

The Elegant Solution

❌ Before: Variable Soup

const user = await fetchUser(id);
const profile = user.profile;
const settings = profile.settings;
const theme = settings.theme;
const isDark = theme === 'dark';
if (isDark) {
  applyDarkMode();
}
const formatted = formatUser(user);
return formatted;

✅ After: Crystal Clear Intent

import { asScope } from 'kotlinify-ts/scope';

return await asScope(fetchUser(id))
  .apply(u => {
    if (u.profile.settings.theme === 'dark') {
      applyDarkMode();
    }
  })
  .let(user => formatUser(user))
  .value();

// One expression. Clear data flow. Easy to refactor.

Why This Matters

  • Reduced Cognitive Load: No more tracking 10 temporary variables in your head
  • Refactoring Safety: Move entire transformation chains without breaking dependencies
  • Clear Intent: Code reads like a story, not a puzzle
  • Fewer Bugs: Can't accidentally use the wrong variable when there aren't any

Installation & Setup

One import unlocks the full power of chainable scope functions

🚀 Chainable Scope Functions

The true power of kotlinify-ts comes from its chainable API. Wrap any value with asScope() to create elegant transformation pipelines with zero prototype pollution.

import { asScope } from 'kotlinify-ts/scope';

// Use asScope() for safe method chaining
const result = asScope(value)
  .let(v => v.transform())
  .also(v => console.log('Debug:', v))
  .let(v => v.toString())
  .value();

// Chain through complex transformations
const processed = asScope(getUserData())
  .let(data => normalize(data))
  .apply(data => {
    data.timestamp = Date.now();
    data.version = '2.0';
  })
  .also(data => cache.store(data))
  .let(data => formatForDisplay(data))
  .value();

// Alternative: Enable global prototype extensions (use with caution)
import { enableKotlinifyExtensions } from 'kotlinify-ts/scope';
enableKotlinifyExtensions();

// Now values have direct access to scope functions
const direct = value.let(v => v * 2).also(console.log);

Why chaining matters: No intermediate variables, clear data flow, easy refactoring, and code that reads like a story.

let & letValue

Transform a value and return the result of the transformation

When to use:

  • Converting nullable values to non-nullable results
  • Executing a block of code only if a value is not null
  • Introducing an expression as a variable in local scope
import { asScope, letValue } from 'kotlinify-ts/scope';

// Method 1: Use asScope() for safe chaining
const formatted = asScope(getUserData())
  .let(data => data.normalize())
  .let(data => data.validate())
  .let(data => JSON.stringify(data))
  .value();

// Method 2: Use standalone function
const upperName = letValue(
  { name: "Alice", age: 30 },
  user => user.name.toUpperCase()
);

// Method 3: With prototype extensions enabled
import { enableKotlinifyExtensions } from 'kotlinify-ts/scope';
enableKotlinifyExtensions();

// Now use directly on values
const result = (5)
  .let(x => x * 2)    // 10
  .let(x => x + 3)    // 13
  .let(x => x ** 2)   // 169
  .let(x => "Result: " + x);

// Real-world: API response processing
const displayName = await asScope(fetchUserProfile(id))
  .let(profile => profile.firstName + " " + profile.lastName)
  .let(fullName => fullName.trim() || "Anonymous")
  .value();

// Chain through async operations
const processed = await asScope(fetchData())
  .let(response => response.json())
  .let(data => data.items)
  .let(items => items.filter(i => i.active))
  .value();

Try it yourself:

// Transform user data with let
const user = { name: "Alice", age: 30 };

const greeting = asScope(user)
  .let(u => `${u.name}, age ${u.age}`)
  .let(text => text.toUpperCase())
  .value();

console.log(greeting);

// Chain multiple transformations
const result = asScope(10)
  .let(x => x * 2)    // 20
  .let(x => x + 5)    // 25
  .let(x => x ** 2)   // 625
  .value();

console.log('Result:', result);

apply

Configure an object and return the same object - perfect for initialization

When to use:

  • Object configuration and initialization
  • Builder pattern implementation
  • Setting multiple properties at once
// Use asScope() for chaining
const user = asScope({})
  .apply(u => {
    u.name = 'John Doe';
    u.email = 'john@example.com';
    u.role = 'user';
  })
  .apply(u => {
    u.preferences = { theme: 'dark', language: 'en' };
  })
  .value();

console.log('User:', user);

// Combine apply with let
const jsonConfig = asScope({ database: {} })
  .apply(cfg => {
    cfg.database.host = 'localhost';
    cfg.database.port = 5432;
    cfg.database.name = 'myapp';
  })
  .let(cfg => JSON.stringify(cfg, null, 2))
  .value();

console.log('Config:', jsonConfig);

Try it yourself:

// Configure an object with apply
const config = asScope({})
  .apply(cfg => {
    cfg.name = 'MyApp';
    cfg.version = '1.0.0';
    cfg.debug = true;
  })
  .value();

console.log('Config:', JSON.stringify(config, null, 2));

// Chain apply with let
const settings = asScope({ theme: 'dark' })
  .apply(s => {
    s.fontSize = 14;
    s.lineHeight = 1.5;
  })
  .let(s => `Theme: ${s.theme}, Font: ${s.fontSize}px`)
  .value();

console.log(settings);

also

Perform side effects like logging or validation and return the original value

When to use:

  • Logging intermediate values in a chain
  • Validation that doesn't transform the value
  • Side effects like caching or analytics
// Use also for side effects (logging, debugging)
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const result = asScope(data)
  .also(arr => console.log('Original:', arr.length, 'items'))
  .let(arr => arr.filter(x => x % 2 === 0))
  .also(arr => console.log('After filter:', arr.length, 'items'))
  .let(arr => arr.map(x => x * 2))
  .also(arr => console.log('After map:', arr))
  .value();

console.log('Final result:', result);

run

Execute a block with 'this' context and return the result - great for computed properties

When to use:

  • Computing values based on object properties
  • When you need 'this' context instead of parameter
  • Complex calculations with multiple object properties
// Use run for calculations with 'this' context
const metrics = { views: 1000, clicks: 50, conversions: 10 };

const report = asScope(metrics)
  .run(function() {
    return {
      total: this.views + this.clicks,
      ctr: (this.clicks / this.views * 100).toFixed(2) + '%',
      quality: this.conversions / this.clicks
    };
  })
  .value();

console.log('Report:', report);

// Calculate area with run
const dimensions = { width: 10, height: 20 };
const area = asScope(dimensions)
  .run(function() {
    return this.width * this.height;
  })
  .value();

console.log('Area:', area);
// Real-world: Complex calculations with context
const summary = await asScope(fetchUserStats(userId))
  .run(function() {
    const total = this.posts + this.comments + this.likes;
    const average = total / 3;
    return {
      engagement: average > 100 ? 'high' : 'normal',
      score: this.posts * 10 + this.comments * 5 + this.likes,
      level: Math.floor(Math.log2(total))
    };
  })
  .also(s => console.log('User summary:', s))
  .value();

// Chain run with other scope functions
const processedStats = asScope(getPlayerStats())
  .run(function() {
    return {
      kda: (this.kills + this.assists) / Math.max(this.deaths, 1),
      winRate: (this.wins / this.games * 100).toFixed(1) + '%',
      avgScore: this.totalScore / this.games
    };
  })
  .apply(stats => {
    stats.rank = calculateRank(stats.kda);
  })
  .also(stats => saveToLeaderboard(stats))
  .value();

Note: Use regular function syntax, not arrow functions, to access 'this' context

withValue

Like run, but with clearer syntax when the receiver is a parameter

import { withValue } from 'kotlinify-ts/scope';

// Execute multiple operations on an object
const htmlContent = withValue(
  { title: 'Hello', body: 'World', footer: '2024' },
  function() {
    return `
      <article>
        <h1>${this.title}</h1>
        <main>${this.body}</main>
        <footer>© ${this.footer}</footer>
      </article>
    `;
  }
);

// Complex DOM manipulation
document.body.appendChild(
  withValue(document.createElement('button'), function() {
    this.textContent = 'Click me';
    this.className = 'btn btn-primary';
    this.onclick = () => alert('Clicked!');
    return this;
  })
);

// Building SQL queries dynamically
const query = withValue(
  { table: 'users', conditions: [], joins: [] },
  function() {
    this.conditions.push('active = true');
    this.conditions.push('created_at > NOW() - INTERVAL 30 DAY');
    this.joins.push('LEFT JOIN profiles ON users.id = profiles.user_id');

    return `
      SELECT * FROM ${this.table}
      ${this.joins.join(' ')}
      WHERE ${this.conditions.join(' AND ')}
    `;
  }
);

Null-Safe Variants

Handle nullable values gracefully without explicit null checks

Available for all scope functions: letOrNull, applyOrNull, alsoOrNull, runOrNull

These variants automatically handle null/undefined, returning null instead of throwing errors

import { letOrNull, applyOrNull, alsoOrNull, runOrNull } from 'kotlinify-ts/scope';

// Method 1: Use standalone null-safe functions
const userAge = letOrNull(getUserOrNull(id), user => user.age);

const configuredWidget = applyOrNull(findWidget(id), widget => {
  widget.color = 'blue';
  widget.size = 'large';
});

// Method 2: With prototype extensions (requires enableKotlinifyExtensions())
import { enableKotlinifyExtensions } from 'kotlinify-ts/scope';
enableKotlinifyExtensions();

// Safe chaining with nullable values - elegant and safe!
const result: string | null = maybeGetUser()
  ?.letOrNull(u => u.profile)
  ?.letOrNull(p => p.settings)
  ?.letOrNull(s => s.theme)
  ?.alsoOrNull(t => console.log('Theme:', t));

// Real-world: Safe API data processing
const displayData = fetchApiResponse()
  ?.letOrNull(res => res.data)
  ?.applyOrNull(data => {
    data.timestamp = Date.now();
    data.processed = true;
  })
  ?.letOrNull(data => formatForDisplay(data))
  ?? 'No data available'; // Fallback value

// alsoOrNull - Side effects only if not null
const cachedValue = computeExpensiveValue()
  ?.alsoOrNull(value => cache.set(key, value));

// runOrNull - Compute only if not null
const fullName = findUserProfile(id)
  ?.runOrNull(function() {
    return this.firstName + ' ' + this.lastName;
  });

// Combine with other utility functions
import { takeIf } from 'kotlinify-ts/nullsafety';

const processed = getUserInput()
  ?.letOrNull(input => input.trim())
  ?.let(s => takeIf(s, s => s.length > 0))
  ?.let(s => s.toLowerCase())
  ?.also(s => log('Processing:', s))
  ?? 'default';

Advanced Patterns

Combine scope functions for powerful, expressive code

Pipeline Pattern

import { asScope } from 'kotlinify-ts/scope';

// Clean, readable data processing pipeline
const apiResult = await asScope(fetchRawData())
  .let(raw => parseJSON(raw))
  .also(data => console.log('Parsed:', data))
  .let(data => validateSchema(data))
  .also(valid => metrics.recordValidation(valid))
  .apply(data => {
    data.processed = true;
    data.timestamp = Date.now();
  })
  .let(data => compress(data))
  .also(compressed => cache.store(compressed))
  .value();

// Complex transformation pipeline
const processedData = asScope(rawData)
  .let(data => normalizeData(data))
  .also(d => validateData(d))
  .let(normalized => enrichData(normalized))
  .also(d => logProcessing(d))
  .let(enriched => transformData(enriched))
  .apply(result => {
    result.processedAt = Date.now();
    result.version = '2.0';
  })
  .value();

// Async pipeline with error handling
const result = await asScope(fetchUser(id))
  .let(user => enrichUserData(user))
  .also(user => console.log('Enriched:', user.id))
  .let(user => validateUserPermissions(user))
  .also(validated => auditLog.record(validated))
  .let(user => prepareUserResponse(user))
  .value();

Builder Pattern

import { asScope } from 'kotlinify-ts/scope';

// Building complex objects fluently
const request = asScope({})
  .apply(r => {
    r.method = 'POST';
    r.url = '/api/users';
  })
  .apply(r => {
    r.headers = {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + token
    };
  })
  .apply(r => {
    r.body = JSON.stringify({ name, email });
  })
  .also(r => console.log('Sending request:', r))
  .let(r => fetch(r.url, r))
  .value();

Conditional Execution

import { asScope, also } from 'kotlinify-ts/scope';

// Execute different branches based on conditions
const result = asScope(getValue())
  .let(v => v > 10 ? v * 2 : v)
  .also(v => {
    if (v > 100) console.warn('Large value:', v);
  })
  .let(v => shouldCache ? also(v, val => cache.set(key, val)) : v)
  .apply(v => {
    if (needsFormatting) {
      v.formatted = true;
      v.display = formatValue(v);
    }
  })
  .value();

Quick Reference

Returns Transformation Result

letValue(value, fn)→ fn(value)
run(value, fn)→ fn.call(value)
withValue(value, fn)→ fn.call(value)

Returns Original Value

apply(value, fn)→ value
also(value, fn)→ value

Decision Tree

Need the transformed result? → Use letValue or asScope().let()

Configuring an object? → Use apply

Logging or side effects? → Use also

Need 'this' context? → Use run or withValue

Handling nullable values? → Use *OrNull variants

Want method chaining? → Use asScope() for safe chaining

Next Steps