6 Advanced Applications of Function Types
This chapter deepens the exploration of function types by showing how treating functions as first-class values unlocks concise solutions to common design and concurrency problems. It connects higher-order functions to practical patterns—enhanced object behavior via decorators, encapsulated state via closures, resumable computations via generators, and readable asynchronous flows via promises and async/await. Throughout, the core idea is that functions can be passed, returned, and composed to replace verbose structures with simpler, type-safe building blocks.
The discussion begins by contrasting an object-oriented decorator with a functional alternative: instead of new interfaces and classes, a function decorator returns a new function that augments behavior (for example, adding singleton semantics) while capturing state through a closure. This leads into closures and lambda capture—functions that retain access to their creation environment—illustrated with counters implemented four ways: a fragile global counter, an encapsulated class with a next() method, a compact closure-based counter that preserves private state per instance, and a generator-based, resumable counter that yields values across invocations. These examples show how closures provide object-like statefulness without extra types, and how generators express iterative logic in a linear, readable style.
The second half focuses on asynchronous work for responsiveness, explaining synchronous versus asynchronous execution and the two models that underpin it: threads (true parallelism with synchronization costs) and event loops (single-threaded scheduling that interleaves tasks). It progresses from callbacks (and their deep nesting) to promises, covering promise states (pending, settled, rejected), creation via resolve/reject, error propagation with catch, and higher-level composition with all and race. Finally, async/await offers clearer syntax that reads like sequential code while compiling to promise chains, making complex flows—requesting user input, sequencing dependent steps, and coordinating concurrent tasks—both expressive and maintainable.
Decorator pattern: an IComponent interface, a concrete implementation via ConcreteComponent, and a Decorator which enhances an IComponent with additional behavior
Decorator pattern for the widget factory. IWidgetFactory is the interface, WidgetFactory is a concrete implementation, SingletonDecorator adds singleton behavior to an IWidgetFactory.
Functional decorator: we now only have a makeWidget() function and a singletonDecorator() function.
A simple function which returns a closure: a lambda which references a variable local to the function. Even after getClosure() returns, the variable is still referenced by the closure, so it outlives the function in which it appeared.
It’s important to understand that each closure (in our case counter1 and counter2) ends up with a different n. Whenever we call makeCounter(), a new n is initialized to 1 and captured by the returned closure. Since they are separate values, they don’t interfere with each other.
createThread() creates a new thread. The original thread continues to execute operation1() then operation2() while the new thread executes longRunningOperation() in parallel.
countDown() counts one step, then yields and allows other code to run. It also enqueus another call to countDown() with the decremented counter value. If the counter reaches 0, countDown() doesn’t enqueue another call to itself.
Each counter runs then enqueues another operation. Execution proceeds in the order in which operations are enqueued. Everything runs on a single thread.
getUserName() enqueues the code to get the user name returns a Promise<string>. The caller of getUserName() can call then() on the promise to hook up the getUserEmail() continuation – code to be run once we have a user name. At some later time, the code to get the user name runs and calls resolve() with the user name. At this point, the continuation getUserEmail() gets called with the now-available user name.
A promise starts in the pending state (getUserName() scheduled the code to prompt the user, but question() hasn’t returned yet). resolve() transitions it to the settled state and invokes a continuation if one is provided (after the user provided their name). A value is available so the continuation can be called (getUserEmail() in our case). reject() transitions the promise to the rejected state and invokes an error-handling continuation if one is provided. A value is not available, a reason for the error is available instead.
The different ways to combine a promise. Then: Promise 1 settles and hands out Value 1 to Promise 2; Promise 2 settles and hands out Value 2 to Promise 3. All: Promises 1, 2, and 3 settle. Once all of them are settled, Promise.all gets all their values and can proceed settling its own value. Race: One of the promises settles first (in this case Promise 2). Promise.race gets Value 2 and can proceed settling its own value.
FAQ
What is the decorator pattern and how can I implement it functionally in TypeScript?
The decorator pattern adds behavior to an existing component without modifying its class. Functionally, you can replace an interface-based factory with a function type and return a new function that adds behavior via a closure:type WidgetFactory = () => Widget;
function singletonDecorator(factory: WidgetFactory): WidgetFactory {
let instance: Widget | undefined;
return () => (instance ??= factory());
}
This returned function decorates the original factory by ensuring only one instance is created.When should I prefer a functional decorator over an object-oriented one?
Use a functional decorator when you’re decorating a single operation (e.g., a factory function of type () => T). It’s concise and leverages first-class functions and closures. Prefer the OO version when you need to decorate multiple methods at once (i.e., when an interface exposes several operations that must be coherently wrapped).What is a closure (lambda capture), and how is it used in this chapter?
A closure is a function bundled with its creation-time environment. Variables from the outer scope are “captured” and kept alive after the outer function returns. In singletonDecorator, the returned lambda captures both the original factory and the instance variable so it can preserve and reuse state across calls.How can I implement a counter using a generator (resumable function)?
Use a generator to yield values and resume where it left off:function* counter(): IterableIterator<number> {
let n = 1;
while (true) yield n++;
}
const c = counter();
c.next(); // { value: 1, done: false }
The function type is () => IterableIterator<number>; consumption uses .next().What are the trade-offs among the four counter implementations?
- Global function + global state: simplest but leaky state and only one counter instance - OO class: encapsulates state, supports many instances; slightly more boilerplate - Functional closure: concise; each returned function captures its own private state - Generator: natural, readable sequence logic via yield; consumed through an iterator interfaceWhat’s the difference between threads and event loops for asynchronous execution?
- Threads: can run code in parallel across CPU cores; good for CPU-bound work; require synchronization and can suffer deadlocks/livelocks - Event loop: single-threaded queue that interleaves tasks; great for I/O-bound work without synchronization; CPU-heavy tasks still block the loopHow do callbacks work and what is their main drawback?
A callback is a function you pass to an async operation to be invoked when the result arrives. They enable non-blocking sequencing, but chaining many async steps leads to deeply nested code (“callback hell”) that is harder to read and compose (e.g., joining independent async operations).How do promises improve async code and how do I chain them?
Promises represent values that will be available in the future. Use then() to attach continuations and catch() for errors. Each then() returns a new promise, enabling flat, readable chains. Returning a plain value inside then() is auto-wrapped into a promise.getUserName()
.then(name => { console.log(`Hi ${name}!`); return getUserBirthday(name); })
.then(bday => getUserEmail(bday))
.catch(err => console.error(err));
How do I create a Promise and handle errors with resolve/reject?
Wrap the async operation and call resolve(value) on success or reject(reason) on error:function getUserName(): Promise<string> {
return new Promise((resolve, reject) => {
rl.question("What is your name? ", name => {
rl.close();
name ? resolve(name) : reject("Name can't be empty");
});
});
}
Rejected promises propagate down the chain to the nearest catch().
Programming with Types ebook for free