value-object-cache
Version:
A value object cache that can be used to make value objects behave like primitive types, i.e. if two variables `a` and `b` point to instances of the same class and have the same value, then `a === b`, otherwise `a !== b`.
81 lines (79 loc) • 4.89 kB
text/typescript
type Value = Primitive | ValueArray | ValueObject;
type Primitive = string | number | boolean | symbol | bigint | null | undefined;
interface ValueArray extends ReadonlyArray<Value> {
}
declare const VALUE_OBJECT_BRAND: unique symbol;
declare abstract class ValueObject<T extends object = object> {
protected readonly props: Readonly<T>;
readonly [VALUE_OBJECT_BRAND] = true;
protected constructor(props: Readonly<T>, values: ValueArray);
}
type ReadonlyValue<T extends Value> = T extends readonly [infer U extends Value, ...(infer R extends readonly Value[])] ? R extends [...never[]] ? readonly [ReadonlyValue<U>] : readonly [ReadonlyValue<U>, ...ReadonlyValue<R>] : T extends readonly (infer U extends Value)[] ? readonly ReadonlyValue<U>[] : T;
declare function isPrimitive(x: unknown): x is Primitive;
declare function isValueArray(x: unknown): x is ValueArray;
declare function isValueObject(x: unknown): x is ValueObject;
declare function isValue(x: unknown): x is Value;
/** A cache tree node used to store an object {@link WeakRef} and a {@link Map} of child nodes, both optional. */
interface CacheTreeNode {
children: Map<unknown, CacheTreeNode> | null;
instanceRef: WeakRef<object> | null;
}
/**
* A value object cache that can be used to make value objects behave like primitive types, i.e. if two variables `a`
* and `b` point to an instance of the same class and have the same value, then `a === b`, otherwise `a !== b`.
*
* To achieve this, the cache can be queried with three arguments: a class constructor, an array of values, and a
* factory function. Values represent the "identity" of an instance: all calls to the cache with the same constructor
* and instance parameters (according to the [same-value-zero equality](
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#same-value-zero_equality))
* will return the same instance. If the cache already contains an instance of this class with the same values, then it
* is returned. Otherwise, the provided factory is called to create a new instance, which is then stored in the cache
* and returned - all the following calls to the cache with the same constructor and values will now return this
* instance until it is garbage-collected. Because, as the cache only stores weak references to the instances and their
* constructors, they can still be garbage-collected once they become unreachable.
*
* While value objects aren't usually expected to have the same identity when they're equal, making sure they do can
* make life easier in situations where specifying a custom equality function isn't practical or even doable, such as
* when using React hooks like {@link useCallback}, {@link useMemo}, {@link useEffect}, etc.
*
* // TODO update doc
*
* @see https://en.wikipedia.org/wiki/Value_object
*
* @example
* ```ts
* abstract class Dimension<Unit extends string> {
* constructor(
* readonly scalar: number,
* readonly unit: Unit,
* ) {
* Object.freeze(this);
* return valueObjectCache.getInstance(this.constructor, [scalar, unit], () => this);
* }
* }
*
* type LengthUnit = 'mm' | 'm' | 'km';
* class Length extends Dimension<LengthUnit> {}
* class OtherLength extends Dimension<LengthUnit> {}
*
* console.log(new Length(1, 'm') === new Length(1, 'm')); // outputs 'true'
* console.log(new Length(1, 'm') === new Length(2, 'm')); // outputs 'false'
* console.log(new Length(1, 'm') === new OtherLength(1, 'm')); // outputs 'false'
* ```
*/
declare const valueObjectCache: {
readonly "__#1@#rootNode": CacheTreeNode;
readonly "__#1@#finalizationRegistry": FinalizationRegistry<readonly unknown[]>;
"__#1@#get"<T extends object>(constructor: Function, values: ValueArray, factory: () => T): T;
/** Look for an instance of the provided class constructor matching the provided values. If a matching instance is
* found then it is returned, otherwise the factory function is called to create a new instance, which is then stored
* in the cache and returned - all future calls to this method made with the same constructor and values will return
* this instance until it is garbage-collected. */
getObject<const T extends object>(constructor: Function, values: ValueArray, factory: () => T): T;
/** Look for an {@link Array} containing a specific list of values in the cache. If a matching {@link Array} is found
* then it is returned, otherwise a new {@link Array} is stored in the cache and returned. All returned arrays are
* frozen (readonly). */
getArray<const T extends ValueArray>(values: T): ReadonlyValue<T>;
getValue<const T extends Value>(value: T): ReadonlyValue<T>;
};
export { type Primitive, type ReadonlyValue, type Value, type ValueArray, ValueObject, isPrimitive, isValue, isValueArray, isValueObject, valueObjectCache };