mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
JSON.stringify answers undefined for an undefined, function, or symbol root. Through `unknown` (or a generic instantiated with it) the dyn-root lowering converted the runtime result to a string, so the result was the text "undefined": `text === undefined` was false and `typeof text` was "string". Where the result reaches a use that distinguishes undefined from that text (undefined/null comparisons, typeof, ??, ?., truth tests, a return from a function declaring undefined, or an unannotated local binding used that way), the call now keeps the checked `string | undefined` result the replacer path already produces; later string uses of such a binding narrow by the checker's flow type. Other uses keep the existing string ABI, so programs that cannot observe the difference compile unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
39 lines
1.7 KiB
TypeScript
39 lines
1.7 KiB
TypeScript
// JSON.stringify of an unknown (or generic) root whose value is undefined,
|
|
// a function, or a symbol returns undefined, not text. Wherever the program
|
|
// can tell the two apart (undefined/null comparisons, typeof, ??, ?., truth
|
|
// tests, returns declaring undefined, and bindings used that way) the
|
|
// native result is the actual undefined. Nested values follow Node's
|
|
// member rules: dropped from objects, null in arrays.
|
|
const u: unknown = undefined;
|
|
const f: unknown = () => 1;
|
|
const s: unknown = Symbol("x");
|
|
const ok: unknown = { a: [1, undefined] };
|
|
for (const v of [u, f, s, ok]) {
|
|
const t = JSON.stringify(v);
|
|
console.log(t === undefined, typeof t, t ?? "fb", t?.length, t == null, t !== undefined);
|
|
}
|
|
for (const v of [u, ok]) {
|
|
const t = JSON.stringify(v);
|
|
if (t) console.log("truthy", t.length);
|
|
else console.log("falsy");
|
|
console.log(!t, t ? "y" : "n", t || "or", t && "and");
|
|
}
|
|
console.log(JSON.stringify(u) === undefined, typeof JSON.stringify(f), JSON.stringify(s) ?? "fb2", JSON.stringify(u)?.length);
|
|
console.log(JSON.stringify(u) || "or2", JSON.stringify(ok) || "or3", !JSON.stringify(u));
|
|
function raw<T>(v: T): string | undefined {
|
|
return JSON.stringify(v);
|
|
}
|
|
console.log(typeof raw(u), raw(ok));
|
|
function ser<T>(v: T): string {
|
|
const text = JSON.stringify(v);
|
|
if (text === undefined) return "undefined result";
|
|
return `string ${text} ${text.length}`;
|
|
}
|
|
console.log(ser(u), ser({ a: 1 }), ser(1), ser("s"), ser(ok));
|
|
// Unobserved uses keep the plain string path.
|
|
const plain = JSON.stringify(ok);
|
|
console.log(plain.length, plain, `${JSON.stringify(u)}`, "x" + JSON.stringify(u));
|
|
// A module-scope binding keeps the string slot; direct uses still observe.
|
|
const top = JSON.stringify(ok);
|
|
console.log(top.length, JSON.stringify(u) === undefined);
|