Skip to content

Instantly share code, notes, and snippets.

@sebinsua
Last active September 22, 2023 15:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sebinsua/a5fe809e9e44f1b5e9f54ed3fa5d091d to your computer and use it in GitHub Desktop.
Save sebinsua/a5fe809e9e44f1b5e9f54ed3fa5d091d to your computer and use it in GitHub Desktop.
import stringify from "fast-json-stable-stringify";
type ImmutablePrimitive =
| undefined
| null
| boolean
| string
| number
| Function;
export type Immutable<T> = T extends ImmutablePrimitive
? T
: T extends Array<infer U>
? ImmutableArray<U>
: T extends Map<infer K, infer V>
? ImmutableMap<K, V>
: T extends Set<infer M>
? ImmutableSet<M>
: ImmutableObject<T>;
export type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
export type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
const ws = new WeakSet<any>();
const m = new Map<string, any>();
function v<Value>(value: Value): Immutable<Value> {
if (typeof value !== "object" || value === null) {
// @ts-ignore
return value;
}
if (ws.has(value)) {
// @ts-ignore
return value;
}
const key = stringify(value);
if (m.has(key)) {
return m.get(key);
}
ws.add(value);
m.set(key, value);
// @ts-ignore
return value;
}
const a = v({ count: 1 });
const b = v({ count: 1 });
if (a === b) {
console.log(
"When `a` and `b` have value equality we also ensure that they have reference equality."
);
}
@sebinsua
Copy link
Author

sebinsua commented Sep 19, 2023

I had no idea this technique already exists. It's apparently known as "hash consing".

I've made a small modification to the code above to play nicer with GC.

import stringify from "fast-json-stable-stringify";

type ImmutablePrimitive =
  | undefined
  | null
  | boolean
  | string
  | number
  | Function;

export type Immutable<T> = T extends ImmutablePrimitive
  ? T
  : T extends Array<infer U>
  ? ImmutableArray<U>
  : T extends Map<infer K, infer V>
  ? ImmutableMap<K, V>
  : T extends Set<infer M>
  ? ImmutableSet<M>
  : ImmutableObject<T>;

export type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
export type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };

const seen = new WeakSet<any>();
const stringToKeyMap = new Map<string, object>();
const registry = new FinalizationRegistry((heldValue) => {
  stringToKeyMap.delete(heldValue);
});
const objWeakMap = new WeakMap<object, any>();

function v<Value>(value: Value): Immutable<Value> {
  if (typeof value !== "object" || value === null) {
    // @ts-ignore
    return value;
  }

  if (seen.has(value)) {
    // @ts-ignore
    return value;
  }

  const keyStr = stringify(value);
  if (stringToKeyMap.has(keyStr)) {
    const keyObj = stringToKeyMap.get(keyStr);
    if (keyObj) {
      return objWeakMap.get(keyObj);
    }
  }

  seen.add(value);

  const keyObj = {};
  stringToKeyMap.set(keyStr, keyObj);
  objWeakMap.set(keyObj, value);
  registry.register(value, keyStr);

  // @ts-ignore
  return value;
}

const a = v({ count: 1 });
const b = v({ count: 1 });

if (a === b) {
  console.log(
    "When `a` and `b` have value equality we also ensure that they have reference equality."
  );
}

See: https://codesandbox.io/s/fancy-bash-2rx528?file=/src/index.ts

@sebinsua
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment