Identity & types
Key vs instance id, DataTag response inference, layerKey, Register, and DefaultLayerError.
Two opens with the same key share logical identity; each still gets a unique instance id (physical identity). DataTag branding carries the response type through await confirm.open(...) (wired handle or bag-form), so no explicit generic is needed.
Key vs instance id
| Concept | Role |
|---|---|
| Key | Logical identity. Must be JSON-safe (see below). find, upsert, and gcTime operate on keySignature(key). Multiple live layers may share a key in a parallel stack. |
Instance id (LayerState.id) |
Physical, unique id of one Layer instance (`${hashKey(key)}#n`). Used for rendering keys and instance lookup/removal (getLayer, #remove). |
JSON-safe key domain
hashKey / keySignature serialize with sorted-object JSON.stringify. Keys must stay inside that domain or hashKey, open / find / cancelQueued, and createLayer (eager keySignature) throw LayerKeyError synchronously (narrow with isLayerKeyError).
| Allowed | Rejected |
|---|---|
string, boolean, null, finite number |
undefined (anywhere, including object props) |
| Plain objects / arrays of the above (shared refs / DAGs ok) | NaN, ±Infinity, bigint, symbol, functions |
| Property order in plain objects (sorted for equality) | Non-plain objects (Date, class instances), cycles |
Prefer null or an explicit sentinel (filterId ?? "none") over undefined. Omit optional object fields instead of setting them to undefined.
Wired handle path
Bind a declaration with useLayer(options) (adapter) or createLayer(options, client) (headless). Payload-only open/upsert infer R from the same DataTag on options.key:
import { useLayer } from "@stainless-code/react-layers";
const confirmLayer = useLayer(confirm);
const ok = await confirmLayer.open({ title: "Remove?" });
// ^? boolean
Pass { id: state.id } on dismiss/update/cancelQueued when targeting a specific instance — current on the handle is the caller’s last open, not an implicit default for stack ops.
Bag-form client.open({ …layerOptions, payload }) stays valid — see Migration.
DataTag — typed await open
layerOptions brands its key with a DataTag, so handle open and LayerClient.open infer the response type with no explicit generic:
import { LayerClient, createLayer, layerOptions } from "@stainless-code/layers";
type ConfirmPayload = { title: string };
type ConfirmResponse = boolean;
const client = new LayerClient();
const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
stack: "confirm",
key: ["confirm", "remove"],
});
const c = createLayer(confirm, client);
const ok = await c.open({ title: "Remove?" });
// ^? boolean
Brand a key alone when you don’t need a full options object:
import { layerKey } from "@stainless-code/layers";
const removeKey = layerKey<ConfirmResponse>()(["confirm", "remove"]);
Type helpers exported from core: DataTag, InferDataTagResponse, InferDataTagError, ResponseOf, ErrorOf.
Payload and response are optional
The response type R defaults to void (fire-and-forget), and payload is omittable when the layer needs no input:
import { createLayer } from "@stainless-code/layers";
const about = layerOptions({
stack: "modal",
key: ["about"],
});
const c = createLayer(about, client);
void c.open(); // fire-and-forget
Register and DefaultLayerError
Augment Register once to set the app-wide error type:
export {};
declare module "@stainless-code/layers" {
interface Register {
defaultError: AppError;
}
}
DefaultLayerError defaults to Error unless augmented. open returns Promise<R> — rejections are not typed on the promise (a TypeScript limitation); narrow at runtime with guards like isPayloadValidationError and isLayerCancelledError.
Payload validation
Optional validate on layerOptions or open — a StandardSchemaV1 schema or sync Validator fn ((input) => output, throws on invalid). Parsed synchronously at open; failure rejects with PayloadValidationError and mounts nothing. Input/output inference: InferValidatorInput, InferValidatorOutput.