| name | typed-clone |
| description | Type-safe, extensible deep clone that tracks what was cloned vs returned by reference at the type level. Use when cloning data, implementing the Clone protocol, or working with the Ref/Unref marker types from @aedge-io/typed-clone. |
typed-clone
Type-safe, infallible drop-in replacement for structuredClone that encodes
clone-ability in the type system.
Installation
Node.js / Bun:
(bun | (p)npm) add @aedge-io/typed-clone
Deno:
deno add jsr:@aedge-io/typed-clone
Quick Reference
import {
Clone,
clone,
isInherentlyCloneable,
ref,
unref,
} from "@aedge-io/typed-clone";
import type {
Cloneable,
Cloned,
CloneOptions,
InherentlyCloned,
Ref,
Unref,
} from "@aedge-io/typed-clone";
Clone Priority (top to bottom)
| Priority | Input | Output | Behavior |
|---|
| 1 | null, undefined | same | by value |
| 2 | primitives (string, number, boolean, bigint) | same | by value |
| 3 | symbol, Function, WeakMap, WeakSet, WeakRef, Promise, Generator | Ref<T> | by reference |
| 4 | object with [Clone]() method | return type of [Clone]() | delegates to protocol |
| 5 | Error (all subtypes) | Ref<T> | by reference |
| 6 | Array, Map, Set (exact constructors only) | same container type | element-by-element recursive clone |
| 7 | class with methods | Ref<T> | by reference |
| 8 | plain record / Object.create(null) | { [K]: Cloned<V> } | property-by-property recursive clone |
| 9 | structuredClone-able builtins (Date, RegExp, TypedArrays, ArrayBuffer, ...) | same type | via structuredClone |
| 10 | anything else | Ref<T> | fallback by reference |
Implement the Clone Protocol
import { Clone, clone } from "@aedge-io/typed-clone";
import type { Cloneable, CloneOptions } from "@aedge-io/typed-clone";
class Connection implements Cloneable<Connection> {
constructor(private url: string, private token: string) {}
[Clone](_opts?: CloneOptions): Connection {
return new Connection(this.url, this.token);
}
}
const original = new Connection("https://api.example.com", "secret");
const cloned = clone(original);
The [Clone] return type drives Cloned<T>. It does not have to return the
same type:
class Snapshot implements Cloneable<{ data: number[]; frozen: true }> {
constructor(private data: number[]) {}
[Clone]() {
return { data: [...this.data], frozen: true as const };
}
}
const s = clone(new Snapshot([1, 2, 3]));
[Clone] takes priority over structuredClone, even for builtins:
class SpecialDate extends Date implements Cloneable<SpecialDate> {
[Clone]() {
return new SpecialDate(this.getTime());
}
}
Check for Clone Support at Runtime
import { Clone, isInherentlyCloneable } from "@aedge-io/typed-clone";
if (isInherentlyCloneable(value)) {
const cloned = value[Clone]();
}
Ref and Unref
Ref<T> is a compile-time marker (zero runtime cost) indicating a value was
not cloned. ref() and unref() are identity functions that only change
the type.
const original = { handler: () => 42, name: "test" };
const cloned = clone(original);
cloned.handler === original.handler;
const plain = unref(cloned);
CloneOptions
clone(value, {
depth: 32,
preserveRefs: false,
transfer: [buf],
});
- depth — When exhausted, remaining nested values are returned as
Ref<T>.
Max 500 (TypeScript type inference limit; higher values are clamped).
- preserveRefs — When
true (default), shared sub-objects preserve identity
in the output and circular references are handled. Set to false for up to
~50% faster clones when you can guarantee no circular/shared references.
- transfer — Passed through to
structuredClone for nested ArrayBuffers.
Shared References
Shared sub-objects within a single clone call preserve identity in the output:
const shared = { x: 1 };
const original = { a: shared, b: shared };
const cloned = clone(original);
cloned.a === cloned.b;
cloned.a === original.a;
Known Type Holes
These are cases where Cloned<T> is not able to infer the return type correctly
because the types are structurally indistinguishable.
Container subclasses without custom methods
A subclass of Array/Map/Set that adds no methods is structurally
indistinguishable from the base at the type level, so Cloned<T> predicts a
deep clone. At runtime the subclass constructor is detected and the value is
returned by reference.
class IdArray<T> extends Array<T> {}
const a = new IdArray<number>();
const c = clone(a);
Classes with methods behind deep inheritance (>16 levels)
When the prototype chain exceeds the depth limit, hasCustomMethods can't reach
the level that defines methods. Data classes may be returned by reference
despite the type predicting a clone, and vice versa.
RecordLike classes
A class whose instance shape looks like a plain record to the type system (only
data properties visible) gets Cloned as a plain object type. At runtime, if
the prototype has methods, it's returned by reference instead.
class User {
constructor(readonly name: string) {}
greet() {
return `Hi, ${this.name}`;
}
}
const c = clone(new User("Alice"));
Workaround for all type holes: implement [Clone] on the type, or make
fields private to trigger nominal typing.