Files
scriptc/tests/corpus/6004-class-instance-integrity.ts
T
Chris Tate 91c66e0eba Support Object.freeze, Object.seal, Object.preventExtensions and Object.defineProperty on class instances
Object.freeze, Object.seal and Object.preventExtensions accept a class
instance whose static type names the class, and Object.isFrozen,
Object.isSealed and Object.isExtensible answer for it as Node does.
Object.defineProperty with a string-literal key changes the value and
the writable, enumerable and configurable attributes of a declared
field, with Node's validation and errors.

Only hierarchies passed to these calls pay for them: they get a
per-instance integrity word and an attribute word per redefined field,
and each write to a declared field checks them first. A write to a
read-only field throws Node's TypeError in strict mode code and leaves
the field unchanged in sloppy mode code; a class field initializer on a
non-extensible instance throws "Cannot define property x, object is not
extensible". The instance's checked view carries the attributes, so
Object.keys, JSON.stringify, util.inspect and getOwnPropertyDescriptor
follow them. Readonly<C> over a class maps to the class.

Through an untyped value, freezing, sealing or preventing extensions of
a class instance throws SC2020 instead of doing nothing. The runtime's
"Cannot redefine property" and "Cannot add property" errors name the
property, as Node does.
2026-10-19 04:13:47 -05:00

170 lines
4.3 KiB
TypeScript

export {};
import { inspect } from "node:util";
const message = (run: () => void): string => {
try {
run();
return "no error";
} catch (e) {
return `${e instanceof TypeError ? "TypeError" : "Error"}: ${(e as Error).message}`;
}
};
class Point {
x = 1;
y = 2;
label = "p";
move(dx: number): void {
this.x += dx;
}
}
const p = new Point();
console.log(Object.isFrozen(p), Object.isSealed(p), Object.isExtensible(p));
const same = Object.freeze(p);
console.log(same === p, Object.isFrozen(p), Object.isSealed(p), Object.isExtensible(p));
console.log(message(() => {
p.x = 5;
}));
console.log(message(() => p.move(1)));
console.log(p.x, Object.keys(p));
const q = new Point();
q.x = 9;
console.log(q.x, Object.isFrozen(q));
class Point3 extends Point {
z = 3;
}
const p3 = new Point3();
Object.freeze(p3);
console.log(message(() => {
p3.z = 1;
}));
const asBase: Point = new Point3();
Object.freeze(asBase);
console.log(message(() => {
asBase.y = 0;
}));
console.log(asBase.y);
class Counter {
n = 0;
}
const c = Object.seal(new Counter());
c.n = 5;
console.log(c.n, Object.isSealed(c), Object.isFrozen(c), Object.isExtensible(c));
const d = new Counter();
Object.preventExtensions(d);
d.n = 2;
console.log(d.n, Object.isExtensible(d), Object.isSealed(d), Object.isFrozen(d));
const loose: any = d;
console.log(message(() => {
loose.extra = 1;
}));
class Config {
mode = "dev";
level = 1;
}
const cfg = new Config();
Object.defineProperty(cfg, "mode", { value: "prod", writable: false });
console.log(cfg.mode, Object.keys(cfg), Object.values(cfg));
console.log(message(() => {
cfg.mode = "x";
}));
Object.defineProperty(cfg, "level", { enumerable: false });
console.log(Object.keys(cfg), Object.entries(cfg), cfg.level);
Object.defineProperty(cfg, "level", { configurable: false });
console.log(message(() => Object.defineProperty(cfg, "level", { enumerable: true })));
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(cfg, "mode")));
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(cfg, "level")));
cfg.level = 7;
console.log(cfg.level);
const fz = Object.freeze(new Config());
console.log(message(() => Object.defineProperty(fz, "level", { value: 2 })));
console.log(message(() => Object.defineProperty(fz, "level", { value: 1 })));
console.log(fz.level, JSON.stringify(Object.getOwnPropertyDescriptor(fz, "level")));
class Frozen {
a = 1;
constructor() {
Object.freeze(this);
}
}
const frozenInCtor = new Frozen();
console.log(frozenInCtor.a, Object.isFrozen(frozenInCtor));
console.log(message(() => {
frozenInCtor.a = 2;
}));
class Extended extends Frozen {
b = 2;
}
console.log(message(() => new Extended()));
class Empty {}
const empty = new Empty();
console.log(Object.isExtensible(empty), Object.isSealed(empty), Object.isFrozen(empty));
Object.preventExtensions(empty);
console.log(Object.isExtensible(empty), Object.isSealed(empty), Object.isFrozen(empty));
class Totals {
count = 1;
items: string[] = [];
}
const t = new Totals();
Object.freeze(t);
console.log(message(() => {
t.count += 1;
}));
console.log(message(() => {
t.count++;
}));
console.log(message(() => {
({ value: t.count } = { value: 5 });
}));
t.items.push("still mutable");
console.log(t.count, t.items, Object.isFrozen(t.items));
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(t, "count")));
class Holder<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
const h = Object.freeze(new Holder("text"));
console.log(h.value, Object.isFrozen(h));
const h2 = new Holder(4);
Object.seal(h2);
h2.value = 5;
console.log(h2.value, Object.isSealed(h2));
class Settings {
mode = "dev";
level = 1;
}
const settings = new Settings();
console.log(settings);
Object.defineProperty(settings, "level", { enumerable: false });
console.log(settings, inspect(settings));
console.log([settings], { settings });
class Pair {
x = 1;
y = 2;
}
const pair = new Pair();
Object.freeze(pair);
console.log(pair, Object.isFrozen(pair));
const sealed = new Pair();
(sealed as any).extra = "added";
Object.seal(sealed);
console.log(sealed, Object.isSealed(sealed));
// JSON.stringify reads the same view: non-enumerable fields are skipped.
console.log(JSON.stringify(cfg), JSON.stringify(settings));
console.log(JSON.stringify({ settings, pair, sealed }));
console.log(JSON.stringify([fz, t]));