In my quest to understand how Signals work, I, read Angular Docs and tutes, read tutorials, watched videos, listened to talks etc but still I wanted to understand the magic behind this interesting syntax that allowed me to call an Angular Signal like a function and still invoke an object property as a function.
// Angular Signals
const count = signal(0);
// then call the Signal like a function - reads the value.
console.log('The count is: ' + count());
// invoke Object function
count.set(3);
And that is when i came across the idea Javascript Callable Functions and also reminded me of the fact that in JavaScript all functions are object methods.
Our exercise works based on the premise that a callable object behaves as both as an object and a function. You can access it and assign its properties values obj.bar = value
, call its methods obj.foo()
, but also call the object directly obj()
, as if it were a function.
In order for all these things to make sense we also refer to the classical constructor pattern used in object-oriented programming languages
. The constructor pattern
is a style used to initialise a newly created object once memory has been allocated for it. In our case here, we’re interested in object constructors.
Object constructors will create specific types of objects. The process of creating these objects can accept arguments through a constructor to set the values of member properties and methods when the object is first created. Once the object is ready we can manipulate properties via setters, access them via getters and call object methods.
JavaScript doesn’t support the concept of classes, but it does support special constructor functions that work with objects. By simply prefixing a call to a constructor function with the keyword new
, we can tell JavaScript we would like the function to behave like a constructor and instantiate a new object with the members defined by that function. Inside a constructor, the keyword this
references the new object that’s being created.
I will assume that going on forward you have a good understanding of the constructor pattern (plus you have taken some time to read about it if this is new) as this will form a foundational concept for the idea behind the custom simple signal we are about to build.
The idea we are pursuing here does not imply this is how Angular Signals code looks like, but rather it is exploring an idea in an attempt to understand possible patterns and how Angular Signals concept works.
Alright, lets ride this wave…

First we start at the very basic, how do achieve this in typescript…
obj = new ObjClass();
this.obj() // this call runs a certain function
In TypeScript, to achieve this behavior where calling this.obj()
runs a certain function, you can make use of a getter or explicitly define a method in your class.
Here’s how you can do it:
1. Using a Getter
You can define a getter in your class that executes a specific function when accessed:
class ObjClass {
private someMethod(): void {
console.log("Function is called!");
}
get obj(): () => void {
return this.someMethod.bind(this);
}
}
const instance = new ObjClass();
instance.obj(); // This will log "Function is called!"
2. Defining a Method
Alternatively, you can directly define a method in the class instead of using a getter.
class ObjClass {
obj(): void {
console.log("Function is called!");
}
}
const instance = new ObjClass();
instance.obj(); // This will log "Function is called!"
3. Using a Property with an Arrow Function
If you want more control or don’t need to bind this
manually, you can assign an arrow function to a property:
class ObjClass {
obj = () => {
console.log("Function is called!");
};
}
const instance = new ObjClass();
instance.obj(); // This will log "Function is called!"
Key Differences
Getter: Method to be accessed as a property but still invoke a function.
Method: Conventional way of defining functions in a class.
Arrow Function Property: Ensure
this
is always bound to the class instance.
Lets move a notch higher, to how do we call the instance of a class like a function.
class ObjClass {
obj = () => {
console.log("Function is called!");
};
}
const instance = new ObjClass();
instance(); // This will log "Function is called!"
Using a Function Constructor
You can define the class in a way that the constructor returns a callable function.
class ObjClass {
obj = () => {
console.log("Function is called!");
};
constructor() {
const callable = () => this.obj();
return Object.assign(callable, this); // Combine the function and the class instance
}
}
const instance = new ObjClass() as unknown as () => void;
instance(); // Logs "Function is called!"
The constructor creates a callable function (callable
) that calls the obj
method. Object.assign
merges the class properties and methods into the function, so instance
behaves like both a function and an instance of the class.
Angular Signals
Angular signals were introduced as part of Angular’s reactive system, starting with Angular 16. Signals provide a reactive primitive for managing and tracking state changes, similar to reactive systems in frameworks like SolidJS. They allow you to manage data reactivity without relying heavily on observables or subscriptions.
Key Features of Angular Signals
Declarative Reactivity:
Signals automatically track dependencies and update the UI when values change.Synchronous and Predictable:
Unlike observables, signals are synchronous and provide direct access to the current value without requiring.subscribe()
.Track Dependencies Automatically:
Signals automatically capture dependencies during execution, which simplifies state management.Composable:
You can create derived signals and combine signals to manage more complex state transformations.
In Angular a signal can created like so:
import { signal } from '@angular/core';
const count = signal(0);
console.log(count()); // Get the current value: 0
count.set(5); // Update the value
console.log(count()); // Output: 5
For more on signals: Angular Signal Docs
Minimal Custom Signal Implementation using a Class
Lets create an overly simplified version of Angular signals using just a class in TypeScript. The idea is to have reactive primitives that can store a value, update it, and notify listeners (like effects).
class SimpleSignal<T> {
private value: T;
private listeners: (() => void)[] = [];
constructor(initialValue: T) {
this.value = initialValue;
}
// Getter for the current value
get(): T {
return this.value;
}
// Setter for the value, notifying all listeners
set(newValue: T): void {
this.value = newValue;
this.notify();
}
// Subscribe to changes
effect(listener: () => void): void {
this.listeners.push(listener);
// Run the effect once initially
listener();
}
// Notify all listeners of the value change
private notify(): void {
for (const listener of this.listeners) {
listener();
}
}
}
Usage Examples:
Create SimpleSignal
const count = new SimpleSignal(0);
// Access the current value
console.log(count.get()); // Output: 0
2. Update SimpleSignal
count.set(5);
console.log(count.get()); // Output: 5
3. Create an Effect
count.effect(() => {
console.log(`Count changed to: ${count.get()}`);
});
count.set(10); // Logs: "Count changed to: 10"
count.set(20); // Logs: "Count changed to: 20"
Minimal Custom Computed Signal Implementation using a Class
class SimpleComputedSignal<T> {
private computeFn: () => T;
private listeners: (() => void)[] = [];
private cachedValue!: T;
constructor(computeFn: () => T) {
this.computeFn = computeFn;
this.cachedValue = this.computeFn();
}
get(): T {
return this.cachedValue;
}
update(): void {
this.cachedValue = this.computeFn();
this.notify();
}
effect(listener: () => void): void {
this.listeners.push(listener);
listener();
}
private notify(): void {
for (const listener of this.listeners) {
listener();
}
}
}
// Usage:
const baseCount = new SimpleSignal(2);
const doubleCount = new SimpleComputedSignal(() => baseCount.get() * 2);
doubleCount.effect(() => {
console.log(`Double Count: ${doubleCount.get()}`);
});
baseCount.set(3); // Logs: "Double Count: 6"
Minimal Custom Signal Implementation using a Function
Now that we’ve seen how we can do this using a class, let’s see how we can this using function… hint: remember that functions are objects in Javascript
.
function SimpleSignal<T>(initialValue: T) {
let value = initialValue;
const listeners: (() => void)[] = [];
// The callable signal function
const signal = (() => value) as (() => T) & {
set: (newValue: T) => void;
effect: (listener: () => void) => void;
};
// Add the `set` method to update the value and notify listeners
signal.set = (newValue: T) => {
value = newValue;
listeners.forEach((listener) => listener());
};
// Add the `effect` method to register listeners
signal.effect = (listener: () => void) => {
listeners.push(listener);
// Run the effect immediately
listener();
};
return signal;
}
Usage:
// create simple signal
const baseCount = SimpleSignal(0);
// get the value
console.log(baseCount()); // Output: 0
// update value
baseCount.set(5);
console.log(baseCount()); // Output: 5
// add an effect
baseCount.effect(() => {
console.log(`Base count changed to: ${baseCount()}`);
});
baseCount.set(10); // Logs: "Base count changed to: 10"
baseCount.set(20); // Logs: "Base count changed to: 20"
So what have we here:SimpleSignal
Function:
Returns a callable function (
signal
) that holds the current value.The
signal
function is enhanced with additional properties:set
andeffect
.
signal()
:
Acts as a getter for the signal value.
When invoked, it simply returns the current value.
signal.set(newValue)
:
Updates the signal’s value and notifies all registered listeners.
signal.effect(listener)
:
Registers a listener that gets called whenever the signal value changes.
Minimal Custom Computed Signal Implementation using a Function
Lets use this approach to make computed signals as well.
function SimpleComputedSignal<T>(computeFn: () => T) {
const signal = (() => computeFn()) as (() => T)
const listeners: (() => void)[] = [];
signal.effect = (listener: () => void) => {
listeners.push(listener);
listener();
};
const notify = () => {
listeners.forEach((listener) => listener());
};
// Automatically track dependencies and notify listeners
const originalCompute = computeFn;
computeFn = () => {
notify();
return originalCompute();
};
return signal;
}
const baseCount = UpdatedSimpleSignal(2);
const doubleCount = UpdatedSimpleComputedSignal(() => baseCount() * 2);
const baseCount = UpdatedSimpleSignal(2);
const doubleCount = UpdatedSimpleComputedSignal(() => baseCount() * 2);
console.log(`Double count: ${doubleCount()}`);
baseCount.set(4);
console.log(`Double count: ${doubleCount()}`); // Logs: "Double count: 8"
baseCount.set(8);
console.log(`Double count: ${doubleCount()}`); // Logs: "Double count: 16
baseCount.set(16);
console.log(`Double count: ${doubleCount()}`); // Logs: "Double count: 32
Github link: Simple Signal Repo
More reading:
Creating Callable Objects in JavaScript
var obj = new CallableObject(); obj(args);medium.com
JavaScript Design Patterns
Constructor Patternmedium.com
Callable values
(Ad, please don't block.) 27.1 Kinds of functions 27.2 Ordinary functions 27.2.1 Named function expressions (advanced)…exploringjs.com
beautiful...