npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ngx-signal-immutability

v0.0.1

Published

Compile-time and runtime immutability for Angular signals

Downloads

1

Readme

ngx-signal-immutability

A lightweight library introducing compile-time immutability and optionally runtime object deep freezing to Angular signals:

  • Immutability is enforced through the use of a custom type Immutable<T>, rendering all properties of the signal's value type recursively readonly. This means that the compiler will complain if you tried to mutate a property, saving you from runtime bugs. Read about the limitations of the readonly keyword in the deep freeze section
  • There is support for mutating an immutable signal in a mutable fashion through external solutions like immer.js or by employing an inherent yet less optimized built-in mutation function.

Motivation

At the moment, there is no real immutability support for signals out-of-the-box, eventhough there had been experiments in the past. This is unsurprising, given the broad spectrum of applications that signals are intended for:

We specifically didn’t want to "pick sides" in the mutable / immutable data discussion and designed the signal library (and other Angular APIs) so it works with both. — Angular Discussions - Sub-RFC 2: Signal APIs

However, this can potentially result in unexpected behaviors and challenging-to-debug errors. For instance: We might modify a signal containing a reference-type value from any location without utilizing set, update, mutate. This is actually possible without retaining the reference to the actual WritableSignal, hence operaiting on a supposedly readonly signal.

const sig = signal({ something: 'initial' }).asReadonly();
sig().something = 'new';
console.log(sig()); // prints "{ something: 'new' }"

Another reason for this library is to bring the many advantages of immutability into the world of signals:

  • Supports interoperability with rxjs observables, which may relay on a stream of immutable values (see buffer, shareReplay, etc.)
  • Allows for accumulation of singal emmited values over time
  • Helps following unidirectional dataflow principle
  • More closely adheres to the principles of the functional programming paradigm, enhancing predictability in state modification (matter of taste)
  • Improved signal state change detection, since modification of a signal only then fires a changed event notification if the new value is an actual new object

Installation

Install using npm:

npm install ngx-signal-immutability

Usage

Basic

Create an immutable WritableSignal that can be mutated in a mutable fashion:

import { immutableSignal } from 'ngx-signal-immutability';

const sig = immutableSignal({ count: 0 });

// Api is the same as a regular writable signal
// Apply a mutation
sig.mutate(currentState => {
  currentState.count += 1;
});

// Apply an update
sig.update(current => ({
  count: current.count + 1,
}));

// Set a new State
sig.set({ count: 3 });

// It saves you from:
sig().count = 5; // Compile-time Error

sig.update(current => {
  current.count = 2; // Compile-time Error
  return current;
});

// And this won't fire any changed notification envet:
sig.set(sig());
sig.update(current => current);

Make any signal immutable

import { computed, signal } from '@angular/core';
import { immutable } from 'ngx-signal-immutability';

const sig = signal({ count: 0 });

const immutableSig1 = immutable(sig);
const immutableSig2 = immutable(sig.asReadonly());
const immutableSig3 = immutable(computed(() => sig()));

Deep Freezing

TypeScript's structural type system cannot prevent violations of readonly, and therefore Immutable<T> constraints in certain scenarios, such as:

  1. Type Assertions: If you use a type assertion to override the type of an object, you can potentially bypass the readonly or Immutable<T> constraints.
  2. Type Compatibility: TypeScript's structural typing means that objects with readonly properties are compatible with their mutable counterparts, allowing you to bypass immutability checks.
  3. Function Arguments: If you pass a readonly or Immutable<T> object to a function that accepts the non-readonly version of the type, the immutability type constraints are not enforced within the function's scope.

By deep freezing the state, you can attain runtime immutability assurance against direct modifications. However, it's important to acknowledge that this introduces some performance overhead. Consequently, it's particularly well-suited for use during development and debugging stages, read more.

import { signal } from '@angular/core';
import {
  immutable,
  immutableSignal,
  setGlobalImmutableSignalOptions,
} from 'ngx-signal-immutability';

// Option 1: Enable deep freezing globally
setGlobalImmutableSignalOptions({ enableDeepFreezing: true });
const writableImmutableSig1 = immutableSignal({ count: 0 }); // Any update of the value is automatically freezed
const immutableSig1 = immutable(signal({ count: 0 })); // Any derived value is automatically freezed

// Option 2: Enable deep freezing for specifc immutable signals
const writableImmutableSig2 = immutableSignal(
  { count: 0 },
  { enableDeepFreezing: true }
);
const immutableSig2 = immutable(signal({ count: 0 }), {
  enableDeepFreezing: true,
});

Immer.js

When no specific strategy is provided, a basic Clone-and-Mutate approach is employed, leveraging structuredClone - or JSON Parse/Stringify as alternative if structuredClone is unsupported. While this method might be acceptable for various scenarios, it's advisable to consider integrating a specialized library like immer.js for more robustness and speed.

import {
  immutableSignal,
  setGlobalImmutableSignalOptions,
} from 'ngx-signal-immutability';
import { produce } from 'immer';

// Option 1: Set global mutation function
setGlobalImmutableSignalOptions({
  mutationProducerFn: produce,
});

// Option 2: Set mutation func for specific immutable writable signal
const immutableSig = immutableSignal(
  { count: 0 },
  { mutationProducerFn: produce }
);

Built with ❤️ by zuriscript