type-fns 1.21.3 → 1.21.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/companions/asFrozenDeep.d.ts +52 -0
- package/dist/companions/asFrozenDeep.js +344 -0
- package/dist/companions/asFrozenDeep.js.map +1 -0
- package/dist/companions/kindsWithSlotMutators.d.ts +32 -0
- package/dist/companions/kindsWithSlotMutators.js +51 -0
- package/dist/companions/kindsWithSlotMutators.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/types/FrozenDeep.d.ts +63 -0
- package/dist/types/FrozenDeep.js +3 -0
- package/dist/types/FrozenDeep.js.map +1 -0
- package/dist/types/HasMaybe.d.ts +17 -0
- package/dist/types/HasMaybe.js +3 -0
- package/dist/types/HasMaybe.js.map +1 -0
- package/package.json +8 -7
- package/readme.md +55 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { FrozenDeep } from '../types/FrozenDeep';
|
|
2
|
+
/**
|
|
3
|
+
* .what = a companion to the `FrozenDeep` type; freezes a value deep, in place, and returns it
|
|
4
|
+
* typed as `FrozenDeep<T>`
|
|
5
|
+
* .why = the type announces "readonly all the way down"; this enforces it at runtime, so a write
|
|
6
|
+
* the type refuses also throws, and the two cannot drift apart
|
|
7
|
+
*
|
|
8
|
+
* .note = in place, deliberately. it returns the same reference (`asFrozenDeep(x) === x`), since a
|
|
9
|
+
* copy would break identity for every holder of the original, and would leave that
|
|
10
|
+
* original writable
|
|
11
|
+
* .note = all or none. the whole graph is checked before any of it is frozen, so a refusal leaves
|
|
12
|
+
* the value untouched
|
|
13
|
+
* .note = idempotent. a second call on a value it froze returns the same reference, no throw — even
|
|
14
|
+
* from a second loaded copy of this package, via a `Symbol.for` mark on each value
|
|
15
|
+
* .note = a built-in whose mutators write an internal slot — `Map`, `Set`, `WeakMap`, `WeakSet`,
|
|
16
|
+
* `Date`, `DataView`, `RegExp` (`compile`), `ArrayBuffer` (`resize`, `transfer`) — has each
|
|
17
|
+
* mutator refused at runtime with a `ConstraintError` that names the fix, which a bare
|
|
18
|
+
* `Object.freeze` does not. its reads are untouched
|
|
19
|
+
* .note = a write onto a frozen object, array, or function prop throws the engine's own
|
|
20
|
+
* `TypeError` in strict mode (every es module) and is ignored in sloppy mode, per
|
|
21
|
+
* `Object.freeze`. to change a value, copy it and change the copy:
|
|
22
|
+
* `{ ...frozen, page: { ...frozen.page, limit: 50 } }`, `[...frozen.tags, 'push']`
|
|
23
|
+
* .note = a primitive, branded or plain, is returned as-is
|
|
24
|
+
* .note = it reaches non-enumerable own props too, so `new Error('x', { cause })` freezes `cause`.
|
|
25
|
+
* it never enters a function's `prototype`, which every instance of a class shares
|
|
26
|
+
* .note = it throws a `ConstraintError` on what it can not freeze: a typed array with elements, a
|
|
27
|
+
* global or sticky `RegExp` (its reads write `lastIndex`), a slot-mutator built-in already
|
|
28
|
+
* sealed by another freeze, and one with a mutator pinned as an own non-configurable prop.
|
|
29
|
+
* each names the fix, and the `path` in the value where the refused object sits
|
|
30
|
+
* (`value.page.bytes`, `value.byKey.get('sms')`)
|
|
31
|
+
* .note = it freezes every holder's view of a shared sub-object, since it freezes in place: an
|
|
32
|
+
* object the caller shares with another holder is frozen for that holder too
|
|
33
|
+
* .bound = what it does not stop:
|
|
34
|
+
* - the bytes of an `ArrayBuffer`, or of a `DataView`'s buffer, stay writable through any view
|
|
35
|
+
* built on that buffer; a freeze locks the object, never its memory
|
|
36
|
+
* - a class instance's `#private` fields stay writable through its own methods; a private field
|
|
37
|
+
* is no property, so `Object.freeze` can not reach it
|
|
38
|
+
* - a value a getter returns is not frozen, though the type calls it frozen deep; the walk never
|
|
39
|
+
* invokes an accessor, so it runs no caller code
|
|
40
|
+
* - a refused mutator still runs when reached through its prototype, as in
|
|
41
|
+
* `Map.prototype.set.call(frozen, k, v)` or `Reflect.apply`; the shadow is an own prop, and a
|
|
42
|
+
* `Proxy` that would close it breaks identity and `instanceof` (F12)
|
|
43
|
+
*
|
|
44
|
+
* example:
|
|
45
|
+
* ```ts
|
|
46
|
+
* const event = asFrozenDeep({ page: { limit: 10 }, tags: ['sms'] });
|
|
47
|
+
* event.page.limit = 50; // ⛔ tsc refuses; at runtime, a TypeError
|
|
48
|
+
* event.tags.push('push'); // ⛔ tsc refuses; at runtime, a TypeError
|
|
49
|
+
* const next = { ...event, page: { ...event.page, limit: 50 } }; // ✅ a copy is writable
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare const asFrozenDeep: <T>(value: T) => FrozenDeep<T>;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.asFrozenDeep = void 0;
|
|
4
|
+
const helpful_errors_1 = require("helpful-errors");
|
|
5
|
+
const isPresent_1 = require("../checks/isPresent");
|
|
6
|
+
const kindsWithSlotMutators_1 = require("./kindsWithSlotMutators");
|
|
7
|
+
/**
|
|
8
|
+
* .what = the mark `asFrozenDeep` sets on each object whose mutators it refused
|
|
9
|
+
* .why = a second call must skip what a first call froze, and tell it apart from an object sealed
|
|
10
|
+
* by any other freeze, whose mutators can no longer be shadowed
|
|
11
|
+
* .note = `Symbol.for`, so two loaded copies of this package read one mark; a marked object stays
|
|
12
|
+
* idempotent across duplicate installs
|
|
13
|
+
* .note = set as a non-enumerable own prop, so `Object.keys`, json, and the walk skip it
|
|
14
|
+
*/
|
|
15
|
+
const mutatorsRefusedMark = Symbol.for('type-fns.asFrozenDeep.mutatorsRefused');
|
|
16
|
+
/**
|
|
17
|
+
* .what = the slot-mutator kind a value is an instance of; else null
|
|
18
|
+
* .why = one lookup feeds the check pass and the freeze pass alike
|
|
19
|
+
*/
|
|
20
|
+
const getOneKindWithSlotMutators = (input) => kindsWithSlotMutators_1.kindsWithSlotMutators.find((kind) => input.value instanceof kind.of) ?? null;
|
|
21
|
+
/**
|
|
22
|
+
* .what = the mutators of a value that `asFrozenDeep` has yet to refuse; else none
|
|
23
|
+
* .why = a value already marked owes none, and a value of no slot-mutator kind owes none
|
|
24
|
+
*/
|
|
25
|
+
const getAllMutatorsUnrefused = (input) => {
|
|
26
|
+
// a value this package already refused owes none
|
|
27
|
+
if (Object.hasOwn(input.value, mutatorsRefusedMark))
|
|
28
|
+
return [];
|
|
29
|
+
// a slot-mutator kind owes its mutators; any other object owes none
|
|
30
|
+
return getOneKindWithSlotMutators({ value: input.value })?.mutators ?? [];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* .what = true for the one own property the walk must never enter: a function's `prototype`
|
|
34
|
+
* .why = a class's `prototype` holds the methods every instance shares; to freeze it would freeze
|
|
35
|
+
* every instance's methods, far past the value the caller passed
|
|
36
|
+
*/
|
|
37
|
+
const isPrototypeOfFunction = (input) => typeof input.of === 'function' && input.key === 'prototype';
|
|
38
|
+
/**
|
|
39
|
+
* .what = the key and value of every own data property, symbol keys and non-enumerable keys included
|
|
40
|
+
* .why = `FrozenDeep` maps every key deep, so the walk must reach every value the type claims is
|
|
41
|
+
* frozen — `Object.values` skips symbol keys, and enumerability hides `err.cause`
|
|
42
|
+
* .note = data properties only: an accessor is never invoked, so the walk runs no caller code
|
|
43
|
+
* .note = a function's `prototype` stays out of the walk (see `isPrototypeOfFunction`)
|
|
44
|
+
*/
|
|
45
|
+
const getAllOwnDataEntries = (input) => Reflect.ownKeys(input.of)
|
|
46
|
+
.filter((key) => !isPrototypeOfFunction({ of: input.of, key }))
|
|
47
|
+
.map((key) => ({
|
|
48
|
+
key,
|
|
49
|
+
descriptor: Reflect.getOwnPropertyDescriptor(input.of, key),
|
|
50
|
+
}))
|
|
51
|
+
.filter((own) => (0, isPresent_1.isPresent)(own.descriptor) && 'value' in own.descriptor)
|
|
52
|
+
.map((own) => ({ key: own.key, value: own.descriptor?.value }));
|
|
53
|
+
/**
|
|
54
|
+
* .what = a string as a single-quoted js literal
|
|
55
|
+
* .why = a path sits inside the json metadata of a refusal; a double-quoted key would print there
|
|
56
|
+
* as `\"sms\"`, where a single-quoted one prints as `'sms'`
|
|
57
|
+
*/
|
|
58
|
+
const asQuotedLiteral = (input) => `'${input.of.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
59
|
+
/**
|
|
60
|
+
* .what = the path to an own property, from its holder's path, in js accessor notation
|
|
61
|
+
* .why = a refusal deep in a payload must say where it sits, or the caller hunts for it
|
|
62
|
+
* .note = `value.page`, `value.tags[0]`, `value['a-b']`, `value[Symbol(tag)]`
|
|
63
|
+
*/
|
|
64
|
+
const asPathToOwnProperty = (input) => {
|
|
65
|
+
const { holder, key } = input;
|
|
66
|
+
if (typeof key === 'symbol')
|
|
67
|
+
return `${holder.path}[${String(key)}]`;
|
|
68
|
+
if (Array.isArray(holder.value) && /^\d+$/.test(key))
|
|
69
|
+
return `${holder.path}[${key}]`;
|
|
70
|
+
if (/^[A-Za-z_$][\w$]*$/.test(key))
|
|
71
|
+
return `${holder.path}.${key}`;
|
|
72
|
+
return `${holder.path}[${asQuotedLiteral({ of: key })}]`;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* .what = the path to a map value, from the map's path, in js accessor notation
|
|
76
|
+
* .note = `.get('sms')` for a string, number, or boolean key; else the entry's position,
|
|
77
|
+
* `[...value.byKey.values()][0]`, since an object key has no literal to name it
|
|
78
|
+
*/
|
|
79
|
+
const asPathToMapValue = (input) => {
|
|
80
|
+
const { map, key, position } = input;
|
|
81
|
+
if (typeof key === 'string')
|
|
82
|
+
return `${map.path}.get(${asQuotedLiteral({ of: key })})`;
|
|
83
|
+
if (typeof key === 'number' || typeof key === 'boolean')
|
|
84
|
+
return `${map.path}.get(${String(key)})`;
|
|
85
|
+
return `[...${map.path}.values()][${position}]`;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* .what = every object reachable from a value, each once, in visit order, with the path that
|
|
89
|
+
* first reached it
|
|
90
|
+
* .why = a pure read of the graph, so every refusal is found before any freeze happens, and each
|
|
91
|
+
* refusal can name where in the value it sits
|
|
92
|
+
* .note = walks own data values (non-enumerable ones too), map keys and values, and set members;
|
|
93
|
+
* never a function's `prototype`; a typed array's elements
|
|
94
|
+
* are numbers, so its interior is not walked. a weak collection can not be enumerated,
|
|
95
|
+
* so its entries are not walked either
|
|
96
|
+
* .note = a shared sub-object is named by the first path that reached it
|
|
97
|
+
* .note = a general graph walk, private while this is its one caller; lift it to `src/` when a
|
|
98
|
+
* second caller (a deep equal, a deep clone) appears
|
|
99
|
+
*/
|
|
100
|
+
const getAllReachableObjects = (input) => {
|
|
101
|
+
const reached = new Map();
|
|
102
|
+
/**
|
|
103
|
+
* .what = records one value at its path, then descends into what it holds
|
|
104
|
+
* .why = one recursion for every shape; the `reached` check ends a cycle and dedupes a shared
|
|
105
|
+
* sub-object
|
|
106
|
+
*/
|
|
107
|
+
const visit = (step) => {
|
|
108
|
+
// skip a primitive, and a value already reached by another path
|
|
109
|
+
const { value, path } = step;
|
|
110
|
+
if (value === null)
|
|
111
|
+
return;
|
|
112
|
+
if (typeof value !== 'object' && typeof value !== 'function')
|
|
113
|
+
return;
|
|
114
|
+
if (reached.has(value))
|
|
115
|
+
return;
|
|
116
|
+
reached.set(value, path);
|
|
117
|
+
// descend into entries, members, and own data values
|
|
118
|
+
if (ArrayBuffer.isView(value))
|
|
119
|
+
return;
|
|
120
|
+
if (value instanceof Map)
|
|
121
|
+
[...value].forEach(([key, entry], position) => {
|
|
122
|
+
visit({ value: key, path: `[...${path}.keys()][${position}]` });
|
|
123
|
+
visit({
|
|
124
|
+
value: entry,
|
|
125
|
+
path: asPathToMapValue({ map: { path }, key, position }),
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
if (value instanceof Set)
|
|
129
|
+
[...value].forEach((member, position) => visit({ value: member, path: `[...${path}][${position}]` }));
|
|
130
|
+
for (const own of getAllOwnDataEntries({ of: value }))
|
|
131
|
+
visit({
|
|
132
|
+
value: own.value,
|
|
133
|
+
path: asPathToOwnProperty({ holder: { path, value }, key: own.key }),
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
visit({ value: input.from, path: 'value' });
|
|
137
|
+
return [...reached].map(([object, path]) => ({ object, path }));
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* .what = refuses a typed array with elements; else null
|
|
141
|
+
* .why = `Object.freeze` throws on a non-empty typed array, since its index slots can not be sealed
|
|
142
|
+
* .note = a `DataView` has no index slots, so it freezes fine; its setters are refused instead
|
|
143
|
+
*/
|
|
144
|
+
const getOneRefusalOfTypedArray = (input) => {
|
|
145
|
+
const { value, path } = input;
|
|
146
|
+
if (!ArrayBuffer.isView(value))
|
|
147
|
+
return null;
|
|
148
|
+
if (value instanceof DataView)
|
|
149
|
+
return null;
|
|
150
|
+
if (value.byteLength === 0)
|
|
151
|
+
return null;
|
|
152
|
+
return new helpful_errors_1.ConstraintError('asFrozenDeep can not freeze a typed array with elements', {
|
|
153
|
+
path,
|
|
154
|
+
kind: value.constructor.name,
|
|
155
|
+
byteLength: value.byteLength,
|
|
156
|
+
hint: 'convert it to a plain array first, e.g. Array.from(bytes)',
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* .what = refuses a global or sticky `RegExp`; else null
|
|
161
|
+
* .why = each `.exec()` / `.test()` on one writes `lastIndex`, so once frozen, a plain read throws
|
|
162
|
+
*/
|
|
163
|
+
const getOneRefusalOfRegExpWithLastIndex = (input) => {
|
|
164
|
+
const { value, path } = input;
|
|
165
|
+
if (!(value instanceof RegExp))
|
|
166
|
+
return null;
|
|
167
|
+
if (!value.global && !value.sticky)
|
|
168
|
+
return null;
|
|
169
|
+
return new helpful_errors_1.ConstraintError('asFrozenDeep can not freeze a global or sticky RegExp, since each exec writes its lastIndex', {
|
|
170
|
+
path,
|
|
171
|
+
kind: 'RegExp',
|
|
172
|
+
flags: value.flags,
|
|
173
|
+
hint: "copy it without the g and y flags first: new RegExp(re.source, re.flags.replace(/[gy]/g, ''))",
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* .what = a class name with its indefinite article, `a Map` or `an ArrayBuffer`
|
|
178
|
+
* .why = a refusal names the kind in prose; `a ArrayBuffer` reads as a typo
|
|
179
|
+
*/
|
|
180
|
+
const asKindWithArticle = (input) => `${/^[AEIOU]/i.test(input.kind) ? 'an' : 'a'} ${input.kind}`;
|
|
181
|
+
/**
|
|
182
|
+
* .what = refuses a slot-mutator kind sealed by a freeze other than `asFrozenDeep`; else null
|
|
183
|
+
* .why = a sealed object takes no new own props, so its mutators can no longer be shadowed
|
|
184
|
+
*/
|
|
185
|
+
const getOneRefusalOfSealedElsewhere = (input) => {
|
|
186
|
+
const { value, path } = input;
|
|
187
|
+
if (getAllMutatorsUnrefused({ value }).length === 0)
|
|
188
|
+
return null;
|
|
189
|
+
if (Object.isExtensible(value))
|
|
190
|
+
return null;
|
|
191
|
+
const kind = getOneKindWithSlotMutators({ value });
|
|
192
|
+
return new helpful_errors_1.ConstraintError(`asFrozenDeep can not freeze ${asKindWithArticle({ kind: value.constructor.name })} already sealed via Object.freeze, Object.seal, or Object.preventExtensions`, {
|
|
193
|
+
path,
|
|
194
|
+
kind: value.constructor.name,
|
|
195
|
+
hint: `pass it to asFrozenDeep before any other freeze, or copy it first: ${kind?.copy}`,
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* .what = refuses a slot-mutator kind with a mutator pinned as an own non-configurable prop; else null
|
|
200
|
+
* .why = `Object.defineProperty` can not replace it, so the shadow would throw mid-freeze
|
|
201
|
+
*/
|
|
202
|
+
const getOneRefusalOfMutatorPinned = (input) => {
|
|
203
|
+
const { value, path } = input;
|
|
204
|
+
const pinned = getAllMutatorsUnrefused({ value }).find((mutator) => Reflect.getOwnPropertyDescriptor(value, mutator)?.configurable === false);
|
|
205
|
+
if (!pinned)
|
|
206
|
+
return null;
|
|
207
|
+
const kind = getOneKindWithSlotMutators({ value });
|
|
208
|
+
return new helpful_errors_1.ConstraintError(`asFrozenDeep can not freeze ${asKindWithArticle({ kind: value.constructor.name })} whose mutator is pinned as an own non-configurable property`, {
|
|
209
|
+
path,
|
|
210
|
+
kind: value.constructor.name,
|
|
211
|
+
mutator: pinned,
|
|
212
|
+
hint: `copy it first, which carries no own props: ${kind?.copy}`,
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* .what = every check a single object must clear before `asFrozenDeep` freezes it
|
|
217
|
+
* .why = one list, so a new refusal lands as one new entry
|
|
218
|
+
*/
|
|
219
|
+
const freezeRefusalChecks = [
|
|
220
|
+
getOneRefusalOfTypedArray,
|
|
221
|
+
getOneRefusalOfRegExpWithLastIndex,
|
|
222
|
+
getOneRefusalOfSealedElsewhere,
|
|
223
|
+
getOneRefusalOfMutatorPinned,
|
|
224
|
+
];
|
|
225
|
+
/**
|
|
226
|
+
* .what = the first refusal any object of a graph earns, which names the object's path; else null
|
|
227
|
+
* .why = lets the walk refuse the whole graph before it touches any of it
|
|
228
|
+
*/
|
|
229
|
+
const getOneFreezeRefusalOfGraph = (input) => input.reached
|
|
230
|
+
.flatMap((each) => freezeRefusalChecks.map((check) => check({ value: each.object, path: each.path })))
|
|
231
|
+
.find(isPresent_1.isPresent) ?? null;
|
|
232
|
+
/**
|
|
233
|
+
* .what = shadows each named mutator with an own method that throws, then marks the value
|
|
234
|
+
* .why = the only way to refuse `map.set()` or `date.setFullYear()` at runtime and keep the same
|
|
235
|
+
* instance — a `Proxy` or a copy would break identity and `instanceof`
|
|
236
|
+
* .note = the shadows and the mark are non-enumerable, so `Object.keys` and json do not see them
|
|
237
|
+
*/
|
|
238
|
+
const setMutatorsRefused = (input) => {
|
|
239
|
+
// shadow each mutator with a method that names the fix
|
|
240
|
+
const kind = input.value.constructor.name;
|
|
241
|
+
const copy = getOneKindWithSlotMutators({ value: input.value })?.copy;
|
|
242
|
+
for (const mutator of input.mutators)
|
|
243
|
+
Object.defineProperty(input.value, mutator, {
|
|
244
|
+
value: () => {
|
|
245
|
+
throw new helpful_errors_1.ConstraintError(`a frozen ${kind} refuses .${mutator}()`, {
|
|
246
|
+
kind,
|
|
247
|
+
mutator,
|
|
248
|
+
hint: `copy it first, then mutate the copy: ${copy}`,
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
enumerable: false,
|
|
252
|
+
writable: false,
|
|
253
|
+
configurable: false,
|
|
254
|
+
});
|
|
255
|
+
// mark it, so a later call — from this copy of the package or another — skips it
|
|
256
|
+
Object.defineProperty(input.value, mutatorsRefusedMark, {
|
|
257
|
+
value: true,
|
|
258
|
+
enumerable: false,
|
|
259
|
+
writable: false,
|
|
260
|
+
configurable: false,
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* .what = freezes one object; a slot-mutator kind also has its mutators refused, once
|
|
265
|
+
* .why = the single place this module writes to a caller's value
|
|
266
|
+
*/
|
|
267
|
+
const setFrozen = (input) => {
|
|
268
|
+
// refuse a slot-mutator kind's mutators, unless a prior call already did
|
|
269
|
+
const mutators = getAllMutatorsUnrefused({ value: input.value });
|
|
270
|
+
if (mutators.length > 0)
|
|
271
|
+
setMutatorsRefused({ value: input.value, mutators });
|
|
272
|
+
// freeze the object itself; a no-op if already frozen
|
|
273
|
+
Object.freeze(input.value);
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* .what = a companion to the `FrozenDeep` type; freezes a value deep, in place, and returns it
|
|
277
|
+
* typed as `FrozenDeep<T>`
|
|
278
|
+
* .why = the type announces "readonly all the way down"; this enforces it at runtime, so a write
|
|
279
|
+
* the type refuses also throws, and the two cannot drift apart
|
|
280
|
+
*
|
|
281
|
+
* .note = in place, deliberately. it returns the same reference (`asFrozenDeep(x) === x`), since a
|
|
282
|
+
* copy would break identity for every holder of the original, and would leave that
|
|
283
|
+
* original writable
|
|
284
|
+
* .note = all or none. the whole graph is checked before any of it is frozen, so a refusal leaves
|
|
285
|
+
* the value untouched
|
|
286
|
+
* .note = idempotent. a second call on a value it froze returns the same reference, no throw — even
|
|
287
|
+
* from a second loaded copy of this package, via a `Symbol.for` mark on each value
|
|
288
|
+
* .note = a built-in whose mutators write an internal slot — `Map`, `Set`, `WeakMap`, `WeakSet`,
|
|
289
|
+
* `Date`, `DataView`, `RegExp` (`compile`), `ArrayBuffer` (`resize`, `transfer`) — has each
|
|
290
|
+
* mutator refused at runtime with a `ConstraintError` that names the fix, which a bare
|
|
291
|
+
* `Object.freeze` does not. its reads are untouched
|
|
292
|
+
* .note = a write onto a frozen object, array, or function prop throws the engine's own
|
|
293
|
+
* `TypeError` in strict mode (every es module) and is ignored in sloppy mode, per
|
|
294
|
+
* `Object.freeze`. to change a value, copy it and change the copy:
|
|
295
|
+
* `{ ...frozen, page: { ...frozen.page, limit: 50 } }`, `[...frozen.tags, 'push']`
|
|
296
|
+
* .note = a primitive, branded or plain, is returned as-is
|
|
297
|
+
* .note = it reaches non-enumerable own props too, so `new Error('x', { cause })` freezes `cause`.
|
|
298
|
+
* it never enters a function's `prototype`, which every instance of a class shares
|
|
299
|
+
* .note = it throws a `ConstraintError` on what it can not freeze: a typed array with elements, a
|
|
300
|
+
* global or sticky `RegExp` (its reads write `lastIndex`), a slot-mutator built-in already
|
|
301
|
+
* sealed by another freeze, and one with a mutator pinned as an own non-configurable prop.
|
|
302
|
+
* each names the fix, and the `path` in the value where the refused object sits
|
|
303
|
+
* (`value.page.bytes`, `value.byKey.get('sms')`)
|
|
304
|
+
* .note = it freezes every holder's view of a shared sub-object, since it freezes in place: an
|
|
305
|
+
* object the caller shares with another holder is frozen for that holder too
|
|
306
|
+
* .bound = what it does not stop:
|
|
307
|
+
* - the bytes of an `ArrayBuffer`, or of a `DataView`'s buffer, stay writable through any view
|
|
308
|
+
* built on that buffer; a freeze locks the object, never its memory
|
|
309
|
+
* - a class instance's `#private` fields stay writable through its own methods; a private field
|
|
310
|
+
* is no property, so `Object.freeze` can not reach it
|
|
311
|
+
* - a value a getter returns is not frozen, though the type calls it frozen deep; the walk never
|
|
312
|
+
* invokes an accessor, so it runs no caller code
|
|
313
|
+
* - a refused mutator still runs when reached through its prototype, as in
|
|
314
|
+
* `Map.prototype.set.call(frozen, k, v)` or `Reflect.apply`; the shadow is an own prop, and a
|
|
315
|
+
* `Proxy` that would close it breaks identity and `instanceof` (F12)
|
|
316
|
+
*
|
|
317
|
+
* example:
|
|
318
|
+
* ```ts
|
|
319
|
+
* const event = asFrozenDeep({ page: { limit: 10 }, tags: ['sms'] });
|
|
320
|
+
* event.page.limit = 50; // ⛔ tsc refuses; at runtime, a TypeError
|
|
321
|
+
* event.tags.push('push'); // ⛔ tsc refuses; at runtime, a TypeError
|
|
322
|
+
* const next = { ...event, page: { ...event.page, limit: 50 } }; // ✅ a copy is writable
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
const asFrozenDeep = (value) => {
|
|
326
|
+
// find every object the value reaches, with its path
|
|
327
|
+
const reached = getAllReachableObjects({ from: value });
|
|
328
|
+
// refuse the whole graph if any object can not be frozen
|
|
329
|
+
const refusal = getOneFreezeRefusalOfGraph({ reached });
|
|
330
|
+
if (refusal)
|
|
331
|
+
throw refusal;
|
|
332
|
+
// freeze every object
|
|
333
|
+
for (const each of reached)
|
|
334
|
+
setFrozen({ value: each.object });
|
|
335
|
+
/**
|
|
336
|
+
* .why the cast = `FrozenDeep<T>` is a conditional type, so on an unresolved generic typescript
|
|
337
|
+
* defers it and cannot prove `T` satisfies it — a limit of the checker, never a gap in the
|
|
338
|
+
* claim. the walk above is the proof, and it runs first
|
|
339
|
+
* .removal = drops if typescript ever proves a conditional type against its own input
|
|
340
|
+
*/
|
|
341
|
+
return value;
|
|
342
|
+
};
|
|
343
|
+
exports.asFrozenDeep = asFrozenDeep;
|
|
344
|
+
//# sourceMappingURL=asFrozenDeep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asFrozenDeep.js","sourceRoot":"","sources":["../../src/companions/asFrozenDeep.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .what = the built-ins whose mutators write an internal slot, which `Object.freeze` can not stop
|
|
3
|
+
* .why = `map.set()`, `date.setFullYear()`, `view.setInt8()` each change a frozen object in
|
|
4
|
+
* silence; each owes its mutators shadowed with throwers, and a copy recipe for the hint
|
|
5
|
+
* .note = a file of its own so the doc-drift clamp can read the list, not restate it; the package
|
|
6
|
+
* root does not export it
|
|
7
|
+
*/
|
|
8
|
+
export declare const kindsWithSlotMutators: ({
|
|
9
|
+
of: WeakMapConstructor;
|
|
10
|
+
mutators: string[];
|
|
11
|
+
copy: string;
|
|
12
|
+
} | {
|
|
13
|
+
of: WeakSetConstructor;
|
|
14
|
+
mutators: string[];
|
|
15
|
+
copy: string;
|
|
16
|
+
} | {
|
|
17
|
+
of: DateConstructor;
|
|
18
|
+
mutators: string[];
|
|
19
|
+
copy: string;
|
|
20
|
+
} | {
|
|
21
|
+
of: DataViewConstructor;
|
|
22
|
+
mutators: string[];
|
|
23
|
+
copy: string;
|
|
24
|
+
} | {
|
|
25
|
+
of: RegExpConstructor;
|
|
26
|
+
mutators: string[];
|
|
27
|
+
copy: string;
|
|
28
|
+
} | {
|
|
29
|
+
of: ArrayBufferConstructor;
|
|
30
|
+
mutators: string[];
|
|
31
|
+
copy: string;
|
|
32
|
+
})[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.kindsWithSlotMutators = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* .what = the names of every `set*` method on a prototype
|
|
6
|
+
* .why = `Date` and `DataView` each hold a closed family of setters; to read them from the
|
|
7
|
+
* prototype keeps the list exact for the runtime at hand (e.g. `setFloat16` where present)
|
|
8
|
+
*/
|
|
9
|
+
const getAllSettersOf = (input) => Object.getOwnPropertyNames(input.prototype).filter((name) => name.startsWith('set'));
|
|
10
|
+
/**
|
|
11
|
+
* .what = the built-ins whose mutators write an internal slot, which `Object.freeze` can not stop
|
|
12
|
+
* .why = `map.set()`, `date.setFullYear()`, `view.setInt8()` each change a frozen object in
|
|
13
|
+
* silence; each owes its mutators shadowed with throwers, and a copy recipe for the hint
|
|
14
|
+
* .note = a file of its own so the doc-drift clamp can read the list, not restate it; the package
|
|
15
|
+
* root does not export it
|
|
16
|
+
*/
|
|
17
|
+
exports.kindsWithSlotMutators = [
|
|
18
|
+
{ of: Map, mutators: ['set', 'delete', 'clear'], copy: 'new Map(frozen)' },
|
|
19
|
+
{ of: Set, mutators: ['add', 'delete', 'clear'], copy: 'new Set(frozen)' },
|
|
20
|
+
{
|
|
21
|
+
of: WeakMap,
|
|
22
|
+
mutators: ['set', 'delete'],
|
|
23
|
+
copy: 'a new WeakMap, refilled from the keys you hold',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
of: WeakSet,
|
|
27
|
+
mutators: ['add', 'delete'],
|
|
28
|
+
copy: 'a new WeakSet, refilled from the members you hold',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
of: Date,
|
|
32
|
+
mutators: getAllSettersOf({ prototype: Date.prototype }),
|
|
33
|
+
copy: 'new Date(frozen)',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
of: DataView,
|
|
37
|
+
mutators: getAllSettersOf({ prototype: DataView.prototype }),
|
|
38
|
+
copy: 'new DataView(frozen.buffer.slice(0))',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
of: RegExp,
|
|
42
|
+
mutators: ['compile'],
|
|
43
|
+
copy: 'new RegExp(frozen)',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
of: ArrayBuffer,
|
|
47
|
+
mutators: ['resize', 'transfer', 'transferToFixedLength'].filter((name) => name in ArrayBuffer.prototype),
|
|
48
|
+
copy: 'frozen.slice(0)',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
//# sourceMappingURL=kindsWithSlotMutators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kindsWithSlotMutators.js","sourceRoot":"","sources":["../../src/companions/kindsWithSlotMutators.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,MAAM,eAAe,GAAG,CAAC,KAA4B,EAAY,EAAE,CACjE,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAC1D,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CACvB,CAAC;AAEJ;;;;;;GAMG;AACU,QAAA,qBAAqB,GAAG;IACnC,EAAE,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAC1E,EAAE,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAC1E;QACE,EAAE,EAAE,OAAO;QACX,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;QAC3B,IAAI,EAAE,gDAAgD;KACvD;IACD;QACE,EAAE,EAAE,OAAO;QACX,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;QAC3B,IAAI,EAAE,mDAAmD;KAC1D;IACD;QACE,EAAE,EAAE,IAAI;QACR,QAAQ,EAAE,eAAe,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;QACxD,IAAI,EAAE,kBAAkB;KACzB;IACD;QACE,EAAE,EAAE,QAAQ;QACZ,QAAQ,EAAE,eAAe,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC;QAC5D,IAAI,EAAE,sCAAsC;KAC7C;IACD;QACE,EAAE,EAAE,MAAM;QACV,QAAQ,EAAE,CAAC,SAAS,CAAC;QACrB,IAAI,EAAE,oBAAoB;KAC3B;IACD;QACE,EAAE,EAAE,WAAW;QACf,QAAQ,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,uBAAuB,CAAC,CAAC,MAAM,CAC9D,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,WAAW,CAAC,SAAS,CACxC;QACD,IAAI,EAAE,iBAAiB;KACxB;CACF,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,11 +9,15 @@ export * from './checks/isNotPromise';
|
|
|
9
9
|
export * from './checks/isNotUndefined';
|
|
10
10
|
export * from './checks/isOfEnum';
|
|
11
11
|
export * from './checks/isPresent';
|
|
12
|
+
export * from './companions/asFrozenDeep';
|
|
12
13
|
export * from './companions/omit';
|
|
13
14
|
export * from './companions/pick';
|
|
14
15
|
export * from './guards/assure';
|
|
16
|
+
export * from './types/ArrayWith';
|
|
15
17
|
export * from './types/DropFirst';
|
|
16
18
|
export * from './types/Empty';
|
|
19
|
+
export * from './types/FrozenDeep';
|
|
20
|
+
export * from './types/HasMaybe';
|
|
17
21
|
export * from './types/Literalize';
|
|
18
22
|
export * from './types/PickAny';
|
|
19
23
|
export * from './types/PickOne';
|
package/dist/index.js
CHANGED
|
@@ -25,11 +25,15 @@ __exportStar(require("./checks/isNotPromise"), exports);
|
|
|
25
25
|
__exportStar(require("./checks/isNotUndefined"), exports);
|
|
26
26
|
__exportStar(require("./checks/isOfEnum"), exports);
|
|
27
27
|
__exportStar(require("./checks/isPresent"), exports);
|
|
28
|
+
__exportStar(require("./companions/asFrozenDeep"), exports);
|
|
28
29
|
__exportStar(require("./companions/omit"), exports);
|
|
29
30
|
__exportStar(require("./companions/pick"), exports);
|
|
30
31
|
__exportStar(require("./guards/assure"), exports);
|
|
32
|
+
__exportStar(require("./types/ArrayWith"), exports);
|
|
31
33
|
__exportStar(require("./types/DropFirst"), exports);
|
|
32
34
|
__exportStar(require("./types/Empty"), exports);
|
|
35
|
+
__exportStar(require("./types/FrozenDeep"), exports);
|
|
36
|
+
__exportStar(require("./types/HasMaybe"), exports);
|
|
33
37
|
__exportStar(require("./types/Literalize"), exports);
|
|
34
38
|
__exportStar(require("./types/PickAny"), exports);
|
|
35
39
|
__exportStar(require("./types/PickOne"), exports);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iDAA+B;AAC/B,uDAAqC;AACrC,mDAAiC;AACjC,uDAAqC;AACrC,sDAAoC;AACpC,mDAAiC;AACjC,qDAAmC;AACnC,wDAAsC;AACtC,0DAAwC;AACxC,oDAAkC;AAClC,qDAAmC;AACnC,oDAAkC;AAClC,oDAAkC;AAClC,kDAAgC;AAChC,oDAAkC;AAClC,gDAA8B;AAC9B,qDAAmC;AACnC,kDAAgC;AAChC,kDAAgC;AAChC,wDAAsC;AACtC,qDAAmC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iDAA+B;AAC/B,uDAAqC;AACrC,mDAAiC;AACjC,uDAAqC;AACrC,sDAAoC;AACpC,mDAAiC;AACjC,qDAAmC;AACnC,wDAAsC;AACtC,0DAAwC;AACxC,oDAAkC;AAClC,qDAAmC;AACnC,4DAA0C;AAC1C,oDAAkC;AAClC,oDAAkC;AAClC,kDAAgC;AAChC,oDAAkC;AAClC,oDAAkC;AAClC,gDAA8B;AAC9B,qDAAmC;AACnC,mDAAiC;AACjC,qDAAmC;AACnC,kDAAgC;AAChC,kDAAgC;AAChC,wDAAsC;AACtC,qDAAmC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .what = readonly all the way down, with every primitive left exactly as it is
|
|
3
|
+
* .why = typescript's `Readonly<T>` is one level deep, so `frozen.a.b = 1` compiles while a deep
|
|
4
|
+
* runtime freeze throws on it. this type refuses the write at every depth, to match
|
|
5
|
+
* `asFrozenDeep`, which enforces what this type announces
|
|
6
|
+
*
|
|
7
|
+
* .the family = `FrozenDeep<T>` maps any shape to its frozen form:
|
|
8
|
+
* - primitive, branded or plain -> itself. a primitive has no interior to freeze; a branded
|
|
9
|
+
* primitive (`string & { _brand }`) stays assignable back to its brand
|
|
10
|
+
* - function -> its call signature, with its own props readonly
|
|
11
|
+
* - `Map<K, V>` -> `FrozenMap<K, V>`
|
|
12
|
+
* - `Set<T>` -> `FrozenSet<T>`
|
|
13
|
+
* - array or tuple -> a readonly array or readonly tuple; a tuple keeps its arity,
|
|
14
|
+
* labels, and positions
|
|
15
|
+
* - object -> every property readonly, every value frozen deep
|
|
16
|
+
*
|
|
17
|
+
* .note = there is no `FrozenArray`. an array freezes to `readonly T[]` and a tuple to a readonly
|
|
18
|
+
* tuple, so one name could not cover both without the loss of a tuple's shape
|
|
19
|
+
*
|
|
20
|
+
* .bound = what this type does not refuse:
|
|
21
|
+
* - a hand-off to a mutable OBJECT parameter compiles. typescript ignores `readonly` in
|
|
22
|
+
* structural assignability, so `{ readonly a: 1 }` is assignable to `{ a: 1 }`. a mutable
|
|
23
|
+
* ARRAY parameter is refused, since `ReadonlyArray` lacks the mutator methods
|
|
24
|
+
* - a write through `any` compiles. `any` opts out of every check
|
|
25
|
+
* - `Array.isArray(frozen)` narrows to `any[]`, which hands `.push` back. prefer a `typeof` or
|
|
26
|
+
* a shape check on a frozen value
|
|
27
|
+
* - a `Date`, `DataView`, `WeakMap`, `WeakSet`, `RegExp`, or `ArrayBuffer` keeps the object arm,
|
|
28
|
+
* so its mutator methods (`setFullYear`, `setInt8`, `set`, `add`, `compile`, `resize`) still
|
|
29
|
+
* type-check — the object arm guards props, not methods, and a frozen `Date` stays assignable
|
|
30
|
+
* back to `Date`. `asFrozenDeep` refuses each of those calls at runtime with a `ConstraintError`
|
|
31
|
+
* - a class instance's methods still type-check, so a method that writes a `#private` field
|
|
32
|
+
* compiles; no freeze reaches a private field
|
|
33
|
+
* - a getter's value is typed frozen deep, yet `asFrozenDeep` never invokes a getter, so the
|
|
34
|
+
* object it returns stays writable at runtime
|
|
35
|
+
*/
|
|
36
|
+
export type FrozenDeep<T> = T extends string | number | boolean | bigint | symbol | null | undefined ? T : T extends (...args: never[]) => unknown ? FrozenFunction<T> : T extends ReadonlyMap<infer TKey, infer TValue> ? FrozenMap<TKey, TValue> : T extends ReadonlySet<infer TItem> ? FrozenSet<TItem> : T extends object ? {
|
|
37
|
+
readonly [TKey in keyof T]: FrozenDeep<T[TKey]>;
|
|
38
|
+
} : T;
|
|
39
|
+
/**
|
|
40
|
+
* .what = a map whose mutators are absent and whose keys and values are frozen deep
|
|
41
|
+
* .why = `ReadonlyMap<K, V>` alone refuses `.set` yet leaves each value writable; this freezes both
|
|
42
|
+
*/
|
|
43
|
+
export type FrozenMap<TKey, TValue> = ReadonlyMap<FrozenDeep<TKey>, FrozenDeep<TValue>>;
|
|
44
|
+
/**
|
|
45
|
+
* .what = a set whose mutators are absent and whose members are frozen deep
|
|
46
|
+
* .why = `ReadonlySet<T>` alone refuses `.add` yet leaves each member writable; this freezes both
|
|
47
|
+
*/
|
|
48
|
+
export type FrozenSet<TItem> = ReadonlySet<FrozenDeep<TItem>>;
|
|
49
|
+
/**
|
|
50
|
+
* .what = a function with its call signature kept and its own props readonly
|
|
51
|
+
* .why = a function is an object too, and the runtime freeze refuses a write onto its props
|
|
52
|
+
* .note = a function with no own props passes through whole, so its generics and overloads survive.
|
|
53
|
+
* a function WITH own props is rebuilt from `Parameters` / `ReturnType`, since an
|
|
54
|
+
* intersection with `T` would keep each prop writable (a prop is readonly in an
|
|
55
|
+
* intersection only where every member marks it so). the rebuild keeps one call
|
|
56
|
+
* signature, so a generic or overloaded function with own props narrows to its last one
|
|
57
|
+
*/
|
|
58
|
+
type FrozenFunction<T extends (...args: never[]) => unknown> = [
|
|
59
|
+
keyof T
|
|
60
|
+
] extends [never] ? T : ((...args: Parameters<T>) => ReturnType<T>) & {
|
|
61
|
+
readonly [TKey in keyof T]: FrozenDeep<T[TKey]>;
|
|
62
|
+
};
|
|
63
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FrozenDeep.js","sourceRoot":"","sources":["../../src/types/FrozenDeep.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* makes the listed key(s) `K` of `T` optional — the make-maybe companion of the
|
|
3
|
+
* make-required (`HasMetadata`) / omit (`OmitMetadata`) trio.
|
|
4
|
+
*
|
|
5
|
+
* use when a value's key is authored without it, then bound at a later
|
|
6
|
+
* lifecycle stage (e.g. a domain key like `exid` known only at activation).
|
|
7
|
+
*
|
|
8
|
+
* for example, `HasMaybe<{ role: string; exid: string }, 'exid'>` produces
|
|
9
|
+
* `{ role: string; exid?: string }`.
|
|
10
|
+
*
|
|
11
|
+
* note:
|
|
12
|
+
* - this type preserves all non-`K` keys exactly (required keys stay required)
|
|
13
|
+
* - this type only loosens the listed `K` — it does not add keys and does not
|
|
14
|
+
* touch any key outside `K`
|
|
15
|
+
* - `K extends keyof T` — a key not on `T` is a compile error
|
|
16
|
+
*/
|
|
17
|
+
export type HasMaybe<T extends Record<string, any>, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HasMaybe.js","sourceRoot":"","sources":["../../src/types/HasMaybe.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "type-fns",
|
|
3
3
|
"author": "ehmpathy",
|
|
4
4
|
"description": "A set of types, type checks, and type guards for simpler, safer, and easier to read code.",
|
|
5
|
-
"version": "1.21.
|
|
5
|
+
"version": "1.21.5",
|
|
6
6
|
"repository": "ehmpathy/type-fns",
|
|
7
7
|
"homepage": "https://github.com/ehmpathy/type-fns",
|
|
8
8
|
"keywords": [
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"preversion": "npm run prepush",
|
|
56
56
|
"postversion": "git push origin HEAD --tags --no-verify",
|
|
57
57
|
"prepare:husky": "husky install && chmod ug+x .husky/*",
|
|
58
|
-
"prepare:rhachet": "rhachet init --
|
|
58
|
+
"prepare:rhachet": "rhachet init --hooks --roles mechanic behaver driver architect ergonomist reviewer dreamer dispatcher",
|
|
59
59
|
"prepare": "if [ -e .git ] && [ -z $CI ]; then npm run prepare:husky && npm run prepare:rhachet; fi",
|
|
60
60
|
"test:lint:cycles": "dpdm --no-warning --no-tree --exit-code circular:1 --exclude '^$' 'src/**/*.ts'",
|
|
61
61
|
"upgrade:rhachet": "rhachet upgrade"
|
|
@@ -87,12 +87,13 @@
|
|
|
87
87
|
"esbuild-register": "3.6.0",
|
|
88
88
|
"husky": "8.0.3",
|
|
89
89
|
"jest": "30.2.0",
|
|
90
|
-
"rhachet": "1.
|
|
91
|
-
"rhachet-brains-anthropic": "0.4.
|
|
90
|
+
"rhachet": "1.48.0",
|
|
91
|
+
"rhachet-brains-anthropic": "0.4.3",
|
|
92
|
+
"rhachet-brains-fireworksai": "0.2.3",
|
|
92
93
|
"rhachet-brains-xai": "0.3.3",
|
|
93
|
-
"rhachet-roles-bhrain": "0.
|
|
94
|
-
"rhachet-roles-bhuild": "0.
|
|
95
|
-
"rhachet-roles-ehmpathy": "1.
|
|
94
|
+
"rhachet-roles-bhrain": "0.37.2",
|
|
95
|
+
"rhachet-roles-bhuild": "0.21.36",
|
|
96
|
+
"rhachet-roles-ehmpathy": "1.39.1",
|
|
96
97
|
"test-fns": "1.15.8",
|
|
97
98
|
"ts-jest": "29.1.3",
|
|
98
99
|
"ts-node": "10.9.2",
|
package/readme.md
CHANGED
|
@@ -101,7 +101,28 @@ const str: string = strStr[0];
|
|
|
101
101
|
const num: number = numStrStr[0];
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
useful, for example, to change the first parameter of a function while the rest stay the same.
|
|
105
|
+
|
|
106
|
+
### `FrozenDeep`
|
|
107
|
+
|
|
108
|
+
the generic type `FrozenDeep` is readonly all the way down, where `Readonly` stops at one level. every primitive, branded primitives included, stays exactly as it is.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { FrozenDeep } from 'type-fns';
|
|
112
|
+
|
|
113
|
+
type IsoTimeStamp = string & { _dglo: 'iso-time.IsoTimeStamp' };
|
|
114
|
+
type Event = { at: IsoTimeStamp; page: { limit: number }; tags: string[]; seen: Map<string, number> };
|
|
115
|
+
|
|
116
|
+
declare const event: FrozenDeep<Event>;
|
|
117
|
+
event.page.limit = 50; // 🛑 readonly at depth two
|
|
118
|
+
event.tags.push('push'); // 🛑 a readonly array has no push
|
|
119
|
+
event.seen.set('sms', 1); // 🛑 a FrozenMap has no set
|
|
120
|
+
const at: IsoTimeStamp = event.at; // ✅ a branded primitive stays assignable back
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
one bound to know: a frozen object still passes into a parameter typed as a mutable object, since typescript does not check `readonly` on object props at assignment. a frozen array does not pass into a mutable array parameter, so an object with an array field inside is refused there too — the cause is the array, never a brand. declare the parameter `FrozenDeep<T>` to accept it.
|
|
124
|
+
|
|
125
|
+
maps and sets freeze to `FrozenMap` / `FrozenSet`, also exported. pair it with `asFrozenDeep` to enforce the same at runtime.
|
|
105
126
|
|
|
106
127
|
## type guards
|
|
107
128
|
|
|
@@ -207,3 +228,36 @@ const processUser = (input: { uuid: string }) => {
|
|
|
207
228
|
// ...
|
|
208
229
|
};
|
|
209
230
|
```
|
|
231
|
+
|
|
232
|
+
## companions
|
|
233
|
+
|
|
234
|
+
### `asFrozenDeep`
|
|
235
|
+
|
|
236
|
+
the `asFrozenDeep` function freezes a value and every value it reaches, in place, and returns it typed as `FrozenDeep<T>`. a write the type refuses also throws at runtime.
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { asFrozenDeep } from 'type-fns';
|
|
240
|
+
|
|
241
|
+
const event = asFrozenDeep({ page: { limit: 10 }, seen: new Map([['sms', 1]]) });
|
|
242
|
+
event.page.limit = 50; // 🛑 typescript error; at runtime, a TypeError
|
|
243
|
+
event.seen.set('push', 2); // 🛑 typescript error; at runtime, a ConstraintError (a bare Object.freeze permits it)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
it returns the same reference, so any other holder of a sub-object sees it frozen too. it reaches non-enumerable props as well, so `new Error('x', { cause })` freezes `cause`, yet never enters a function's `prototype`, which every instance of a class shares. it is safe on cycles and shared sub-objects, and a second call is a no-op. it checks the whole value before it freezes any of it, so a refusal (a typed array with elements, a global or sticky `RegExp`, or a map sealed by a bare `Object.freeze`) leaves the value untouched. each refusal is a `ConstraintError` that names where the refused object sits and how to fix it:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
✋ ConstraintError: asFrozenDeep can not freeze a typed array with elements
|
|
250
|
+
|
|
251
|
+
{
|
|
252
|
+
"path": "value.event.records[1].raw",
|
|
253
|
+
"kind": "Uint8Array",
|
|
254
|
+
"byteLength": 1,
|
|
255
|
+
"hint": "convert it to a plain array first, e.g. Array.from(bytes)"
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
a bare `Object.freeze` lets a built-in change its internal state through its own methods. `asFrozenDeep` refuses those at runtime: `Map` and `Set` (`set`, `add`, `delete`, `clear`), `WeakMap` and `WeakSet` (`set`, `add`, `delete`), `Date` and `DataView` (every `set*`), `RegExp` (`compile`), and `ArrayBuffer` (`resize`, `transfer`). the type keeps the object arm for all but `Map` and `Set`, so those calls still compile.
|
|
260
|
+
|
|
261
|
+
what it does not reach: the bytes of an `ArrayBuffer` through a view built on it, a class instance's `#private` fields through its own methods, the value a getter returns (it never invokes a getter), and a refused mutator called through its prototype, as in `Map.prototype.set.call(frozen, k, v)`.
|
|
262
|
+
|
|
263
|
+
to change a frozen value, copy it and change the copy: `{ ...event, page: { ...event.page, limit: 50 } }`, or `new Map(event.seen)` for a map.
|