Read fields as undefined before their assignment

A constructor that let other code read a field before assigning it (a base
constructor calling an override, or the constructor calling its own method)
read an empty slot, which crashed the executable for string fields. Node
reads undefined.

Fields assigned after construction can expose the instance now admit
undefined. Allocation writes undefined into them. String, number, boolean,
bigint and union fields read as runtime-optional values, and class, array,
record and collection fields use the deferred-init reads, whose member
access throws a TypeError. Classes whose construction cannot expose the
instance before their assignments keep their layouts.
This commit is contained in:
Chris Tate
2026-10-18 09:14:01 -05:00
parent 8c1d94e72e
commit 27f78a981d
10 changed files with 587 additions and 62 deletions
@@ -124,6 +124,15 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca
continue;
}
if (ts.isExpressionStatement(stmt) && !exposes(stmt.expression, depth)) continue;
// A check that only reads assigned fields and throws or does nothing
// with the receiver (`if (this.size < 0) throw new RangeError()`)
// does not expose the instance; assignments in its branches are not
// definite and still count as exposures.
if (
(ts.isIfStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBlock(stmt)) &&
!exposes(stmt, depth)
)
continue;
return false;
}
return true;
@@ -143,7 +143,7 @@ import { lowerObjectFactoryNew } from "./object-factory-new.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
import { classPrototypeData } from "./class-prototypes.js";
import { constructionEscape } from "./construction-escapes.js";
import { constructionEscape, mentionsReceiver } from "./construction-escapes.js";
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
import { reflectedClassStaticMethodValue } from "./class-method-values.js";
@@ -468,6 +468,11 @@ export interface ClassInfo {
* unassigned read throws the catchable TypeError instead of yielding
* an undefined the declared type cannot hold (SEMANTICS.md). */
deferredInitFields?: Set<string>;
/** Fields (inherited included) that code construction exposes the
* instance to can read before their assignment: the slot is the
* undefined-armed union, allocation writes undefined, and reads yield the
* union value, so an early read sees undefined as in Node. */
earlyReadFields?: Set<string>;
/** True when construction may hand the instance to other code (a method
* call, an argument, a callback, or a base constructor that does) — a
* subclass's own fields can then be read before their assignment. */
@@ -3473,55 +3478,54 @@ export function collectClassShapeInner(
const derived =
decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ??
false;
// Generic instances and mixins share layouts across declarations, so
// they only report whether construction exposes the instance.
const escape: { escapes: boolean; assigned: Set<string> | null } =
familyMode || mixin
? { escapes: derived, assigned: null }
: constructionEscape({
ctor,
fields: fieldOrder.flatMap((f) =>
paramProps.some((pp) => pp.name === f.name)
? []
: [{ name: f.name, initializer: f.initializer }],
),
paramProps: paramProps.map((pp) => pp.name),
derived,
baseEscapes:
base === null ||
callableBase !== undefined ||
factoryBaseExpression !== undefined ||
base.constructionEscapes === true,
isField: (name) =>
fields.has(name) &&
!methods.has(`set:${name}`) &&
!lowerer.findMethodOn(base, `set:${name}`),
privateMethodBody: (call) => {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee)) return null;
const name = callee.name.text;
const declaration = decl.members.find(
(member): member is ts.MethodDeclaration =>
ts.isMethodDeclaration(member) &&
member.body !== undefined &&
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
member.name.text === name,
);
if (!declaration?.body || declaration.asteriskToken) return null;
const modifiers = ts.getModifiers(declaration) ?? [];
if (
modifiers.some(
(m) =>
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
)
// A generic family is not emitted; its instances and each mixin
// application analyze their own construction with their actual base.
const escape: { escapes: boolean; assigned: Set<string> | null } = familyMode
? { escapes: derived, assigned: null }
: constructionEscape({
ctor,
fields: fieldOrder.flatMap((f) =>
paramProps.some((pp) => pp.name === f.name)
? []
: [{ name: f.name, initializer: f.initializer }],
),
paramProps: paramProps.map((pp) => pp.name),
derived,
baseEscapes:
base === null ||
callableBase !== undefined ||
factoryBaseExpression !== undefined ||
base.constructionEscapes === true,
isField: (name) =>
fields.has(name) &&
!methods.has(`set:${name}`) &&
!lowerer.findMethodOn(base, `set:${name}`),
privateMethodBody: (call) => {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee)) return null;
const name = callee.name.text;
const declaration = decl.members.find(
(member): member is ts.MethodDeclaration =>
ts.isMethodDeclaration(member) &&
member.body !== undefined &&
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
member.name.text === name,
);
if (!declaration?.body || declaration.asteriskToken) return null;
const modifiers = ts.getModifiers(declaration) ?? [];
if (
modifiers.some(
(m) =>
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
)
return null;
const isPrivate =
ts.isPrivateIdentifier(declaration.name) ||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
return isPrivate ? declaration.body : null;
},
});
)
return null;
const isPrivate =
ts.isPrivateIdentifier(declaration.name) ||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
return isPrivate ? declaration.body : null;
},
});
// The deferred definite-assignment check: a field on the unguarded
// list passes only with an unconditional `this.x = ...` at the
// constructor's TOP LEVEL — the same standard the JS-class path
@@ -3577,18 +3581,30 @@ export function collectClassShapeInner(
}
}
if (escape.assigned && !inst) {
// Fields assigned after construction can expose the instance (a method
// call, a base constructor that calls an override) can be read while
// Node still holds undefined. Their slots admit undefined, allocation
// writes it, and reads yield it, as in Node.
const earlyReadFields = new Set<string>(base?.earlyReadFields ?? []);
if (escape.assigned && escape.escapes) {
for (const f of fieldOrder) {
if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue;
if (paramProps.some((pp) => pp.name === f.name)) continue;
const declared = fields.get(f.name);
if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime)
if (declared === undefined || !earlyReadable(lowerer, declared)) continue;
// A declared `T | undefined` field already starts undefined.
if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0)
continue;
const armed = lowerer.withUndefinedArm(declared);
if (armed === null || armed.kind !== "union") continue;
const armed = lowerer.runtimeOptionalType(declared);
if (armed.kind !== "union") continue;
fields.set(f.name, armed);
f.type = armed;
deferredInitFields.add(f.name);
// Primitive and union slots yield the undefined value itself.
// Reference slots keep the deferred-init reads: consumers that
// observe undefined (printing, equality, typeof, nullish defaults)
// see it, and member access on it throws a TypeError, as in Node.
if (declared.kind === "union" || PRIMITIVE_EARLY_READS.has(declared.kind))
earlyReadFields.add(f.name);
else deferredInitFields.add(f.name);
}
}
@@ -3693,6 +3709,43 @@ export function collectClassShapeInner(
(name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name),
)
: null;
// Constructor statements that run after the instance may have been
// exposed (a method call, `this` passed along, a base constructor
// that exposes it): a field they create can be read before its
// assignment, while Node holds undefined.
const exposedBefore = new Set<ts.Statement>();
{
let exposed = false;
for (const stmt of ctor?.body?.statements ?? []) {
const expression = ts.isExpressionStatement(stmt) ? stmt.expression : null;
if (
expression &&
ts.isCallExpression(expression) &&
expression.expression.kind === ts.SyntaxKind.SuperKeyword
) {
if (base?.constructionEscapes === true || expression.arguments.some(mentionsReceiver))
exposed = true;
continue;
}
const assigned =
expression &&
ts.isBinaryExpression(expression) &&
expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isPropertyAccessExpression(expression.left) &&
expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
? expression
: null;
if (exposed || (assigned && mentionsReceiver(assigned.right))) exposedBefore.add(stmt);
const plainAssignment =
assigned !== null &&
ts.isPropertyAccessExpression(assigned.left) &&
!mentionsReceiver(assigned.right) &&
!hasAccessor(assigned.left.name.text) &&
!methods.has(assigned.left.name.text) &&
!lowerer.findMethodOn(base, assigned.left.name.text);
if (!plainAssignment && mentionsReceiver(stmt)) exposed = true;
}
}
if (ctor?.body) {
for (const stmt of ctor.body.statements) {
const lhs =
@@ -3763,8 +3816,15 @@ export function collectClassShapeInner(
type = inheritedJsFieldType(lowerer, decl, name, type);
// super() can invoke a subclass setter or method before its
// constructor assignments run. These slots start undefined.
if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name))
type = DYN;
const sealed = sealedPrefix?.created.has(name) === true;
if (base && lowerer.dynConvertible(type) && !sealed) type = DYN;
if (exposedBefore.has(stmt) && !sealed && earlyReadable(lowerer, type)) {
const armed = lowerer.runtimeOptionalType(type);
if (armed.kind === "union") {
type = armed;
earlyReadFields.add(name);
}
}
fields.set(name, type);
fieldOrder.push({ name, type, initializer: undefined });
continue;
@@ -4025,6 +4085,7 @@ export function collectClassShapeInner(
? { classDecorators: { nodes: classDecoratorNodes } }
: {}),
...(deferredInitFields.size > 0 ? { deferredInitFields } : {}),
...(earlyReadFields.size > 0 ? { earlyReadFields } : {}),
...(escape.escapes ? { constructionEscapes: true as const } : {}),
};
// GENERIC members get their declaring-class backlink now that the
@@ -6199,6 +6260,33 @@ function overrideReturnAbi(
return declared;
}
/** Field kinds whose early reads yield the undefined value (earlyReadFields). */
const PRIMITIVE_EARLY_READS: ReadonlySet<IrType["kind"]> = new Set([
"f64",
"bool",
"string",
"bigint",
]);
/** Field types whose slot can admit undefined for reads before the field's
* assignment. Date values are scalars, so their unassigned slot is memory
* safe; checked-dynamic slots already hold undefined. */
function earlyReadable(lowerer: Lowerer, type: IrType): boolean {
if (isUnitType(type)) return false;
switch (type.kind) {
case "dyn":
case "void":
case "date":
case "generator":
case "caught":
return false;
case "object":
return lowerer.classes.get(type.className)?.def.runtime !== true;
default:
return true;
}
}
/** The nearest declaration of `name` at or above `info` — the method a
* receiver of that static class runs when nothing below overrides it. */
export function findMethodOn(
@@ -7628,7 +7716,11 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca
obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc },
className: info.def.name,
field: pp.name,
value: { kind: "varRef", localId: local!.id, type: local!.type, loc },
// An early-readable field's slot admits undefined (earlyReadFields).
value: lowerer.coerceToExpected(
{ kind: "varRef", localId: local!.id, type: local!.type, loc },
info.fields.get(pp.name) ?? local!.type,
),
loc,
});
}
@@ -3133,6 +3133,51 @@ export function lowerTernary(
* the same union (unnarrowed use), to a SUB-union (partial narrowing —
* unrepresentable without a re-tag), or to nothing (`never` in an
* exhaustive default) leaves the expression union-typed. */
/** Uses that read an early-read field's value as Node does when it is
* undefined: printing, templates, operators, tests, member access, calls of
* program functions (whose parameters the optional-value analysis widens),
* and stores into bindings, fields and returns. Other uses, such as library
* arguments, extract the declared type with a check. */
function observesEarlyRead(node: ts.Node): boolean {
let child = node;
let parent = node.parent;
while (parent && ts.isParenthesizedExpression(parent)) {
child = parent;
parent = parent.parent;
}
if (!parent) return false;
if (
ts.isTemplateSpan(parent) ||
ts.isBinaryExpression(parent) ||
ts.isPrefixUnaryExpression(parent) ||
ts.isPostfixUnaryExpression(parent) ||
ts.isTypeOfExpression(parent) ||
ts.isConditionalExpression(parent) ||
ts.isIfStatement(parent) ||
ts.isWhileStatement(parent) ||
ts.isReturnStatement(parent) ||
ts.isVariableDeclaration(parent) ||
ts.isPropertyAssignment(parent) ||
ts.isArrayLiteralExpression(parent) ||
(ts.isPropertyAccessExpression(parent) && parent.expression === child) ||
(ts.isElementAccessExpression(parent) && parent.expression === child)
)
return true;
if (ts.isCallExpression(parent) && parent.arguments.includes(child as ts.Expression)) {
const callee = parent.expression;
if (
ts.isPropertyAccessExpression(callee) &&
ts.isIdentifier(callee.expression) &&
callee.expression.text === "console"
)
return true;
return (
!ts.isPropertyAccessExpression(callee) || callee.expression.kind === ts.SyntaxKind.ThisKeyword
);
}
return false;
}
export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr {
if (
(expr.type.kind === "union" || expr.type.kind === "dyn") &&
@@ -3343,6 +3388,25 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
if (expr.type.kind !== "union") return expr;
if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node))
return expr;
// A field read before its assignment holds undefined although its
// declared type omits it; the declared type is no narrowing proof. A flow
// narrowing past the declared type (`if (this.x !== null)`) extracts the
// narrowed arm with a check, since the slot may still hold undefined.
if (
expr.kind === "fieldGet" &&
lowerer.classes.get(expr.className)?.earlyReadFields?.has(expr.field) === true
) {
const symbol = ts.isPropertyAccessExpression(node)
? lowerer.checker.getSymbolAtLocation(node.name)
: undefined;
const declared = symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null;
const narrowedArm = lowerer.mapTypeOf(lowerer.typeOf(node));
if (narrowedArm === null || narrowedArm.kind === "union") return expr;
if (declared !== null && typeEquals(declared, narrowedArm) && observesEarlyRead(node))
return expr;
const helper = lowerer.narrowedArmHelper(expr.type.unionId, narrowedArm, expr.loc);
return helper ? lowerer.checkedNarrowCall(helper, expr, narrowedArm, expr.loc) : expr;
}
// Represented fields and index values can include undefined even when
// checker inference or lib.d.ts declares strings (RegExp captures).
@@ -15103,6 +15167,20 @@ export function fieldGetExpr(
const helper = lowerer.deferredReadHelper(target.fieldType.unionId, inner, loc);
if (helper) return { kind: "call", callee: helper, args: [read], type: inner, loc };
}
// An early-read field (ClassInfo.earlyReadFields) yields its undefined
// where the use reads it as Node does; elsewhere the declared type is
// extracted with a check.
if (
target.fieldType.kind === "union" &&
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true &&
!observesEarlyRead(blame)
) {
const declared = lowerer.mapTypeOf(lowerer.typeOf(blame));
if (declared && declared.kind !== "union") {
const helper = lowerer.narrowedArmHelper(target.fieldType.unionId, declared, loc);
if (helper) return lowerer.checkedNarrowCall(helper, read, declared, loc);
}
}
return read;
}
return {
@@ -15643,6 +15721,22 @@ function lowerFieldCompoundValue(
if (op === "+" && read.type.kind === "string") {
appended = lowerer.ensureString(rhs, rhsNode ?? access);
value = { kind: "strConcat", left: read, right: appended, type: STRING, loc };
} else if (
op === "+" &&
read.type.kind === "union" &&
lowerer.stripUndefinedArm(read.type).kind === "string" &&
target.container === "class" &&
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true
) {
// A string field read before its assignment holds undefined, which
// concatenates as "undefined".
value = {
kind: "strConcat",
left: lowerer.ensureString(read, access),
right: lowerer.ensureString(rhs, rhsNode ?? access),
type: STRING,
loc,
};
} else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") {
value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc };
} else if (bigintValue) {
@@ -3933,6 +3933,18 @@ export class Lowerer {
if (!s) return null;
return s.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(s) : s;
};
// Fields that can be read before their assignment hold undefined then;
// their reads are runtime-optional values like missing array reads.
for (const info of this.classes.values()) {
if (!info.earlyReadFields || !info.decl?.name) continue;
const classSymbol = symbolOf(info.decl.name);
if (!classSymbol) continue;
const instance = this.checker.getDeclaredTypeOfSymbol(classSymbol);
for (const name of info.earlyReadFields) {
const property = this.checker.getPropertyOfType(instance, name);
if (property) optionalSymbols.add(property);
}
}
const signatureOf = (node: ts.Identifier): FnSig | null => {
const symbol = symbolOf(node);
if (!symbol) return null;
@@ -0,0 +1,93 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule, validateModule } from "../src/index.js";
import type { IrModule } from "../src/ir/ir.js";
async function lower(source: string, file = "main.ts"): Promise<IrModule> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-early-field-reads-"));
try {
const entry = join(dir, file),
output = join(dir, "main.ir.json");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
const module = deserializeModule(await readFile(output, "utf8"));
expect(validateModule(module)).toEqual([]);
return module;
} finally {
await rm(dir, { recursive: true, force: true });
}
}
/** Field name → IR kind for a class. */
function fields(module: IrModule, name: string): string {
const cls = module.classes.find((candidate) => candidate.name === name);
expect(cls, name).toBeDefined();
return cls!.fields
.filter((field) => !field.name.startsWith("%"))
.map((field) => `${field.name}:${field.type.kind}`)
.join(" ");
}
test("fields assigned after construction can call an override admit undefined", async () => {
const module = await lower(`
abstract class Base {
constructor() {
this.describe();
}
abstract describe(): void;
}
class Panel extends Base {
label: string;
width: number;
count = 0;
constructor() {
super();
this.label = "panel";
this.width = 2;
}
describe(): void {
console.log(this.label, this.width, this.count);
}
}
class Plain {
label: string;
width: number;
constructor() {
this.label = "plain";
this.width = 1;
this.describe();
}
describe(): void {
console.log(this.label, this.width);
}
}
new Panel();
new Plain();
`);
expect(fields(module, "Panel")).toBe("label:union width:union count:union");
expect(fields(module, "Plain")).toBe("label:string width:f64");
});
test("JavaScript fields created after the constructor calls a method admit undefined", async () => {
const module = await lower(
`
class Widget {
constructor() {
this.ready = true;
this.render();
this.title = "widget";
}
render() {
console.log(this.ready, this.title);
}
}
new Widget();
`,
"main.js",
);
expect(fields(module, "Widget")).toBe("ready:bool title:union");
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5943-fields-read-before-assignment.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5944-js-fields-read-before-assignment.js"
],
"diags": []
}
@@ -0,0 +1,181 @@
// @transform-types
// Fields that a base constructor (or the constructor itself) can read
// through an override or a method call before they are assigned hold
// undefined then, as in Node, for every field kind. After construction
// the fields read and update normally.
class Point {
x: number;
constructor(x: number) {
this.x = x;
}
}
function show(label: string, value: number): string {
return `${label}=${value}`;
}
abstract class Widget {
log: string[] = [];
constructor() {
this.describe("base");
}
abstract describe(when: string): void;
}
class Panel extends Widget {
label: string;
width: number;
visible: boolean;
origin: Point;
items: number[];
options: { depth: number };
key: string | number;
index: Map<string, number>;
tags: Set<string>;
count = 5;
constructor(label: string, readonly id: string) {
super();
this.label = label;
this.width = 10;
this.visible = true;
this.origin = new Point(1);
this.items = [1, 2];
this.options = { depth: 2 };
this.key = 7;
this.index = new Map([["a", 1]]);
this.tags = new Set(["t"]);
this.describe("ctor");
}
describe(when: string): void {
console.log(when, this.label, this.width, this.visible, this.origin, this.items, this.options, this.key, this.count, this.id);
console.log(when, `${this.label}:${this.width}`, this.width + 1, this.count * 2, this.label === undefined, typeof this.items, this.origin?.x, this.options?.depth ?? -1, this.index?.size, this.tags?.has("t"));
console.log(when, show(this.label, this.width), String(this.key), [this.label].length);
try {
console.log(this.label.length, this.items.length);
} catch (error) {
console.log(when, (error as Error).message);
}
}
grow(): number {
this.count++;
this.width += 5;
this.label += "!";
this.items.push(this.width);
return this.count + this.width + this.items.length;
}
}
const panel = new Panel("main", "p1");
console.log(panel.grow(), panel.label, panel.items, panel.count, panel.width);
class Plain {
total: number;
name: string;
constructor() {
this.total = 1;
this.name = "plain";
this.bump();
}
bump(): void {
this.total++;
}
}
const plain = new Plain();
console.log(plain.total, plain.name);
class Toucher {
constructor() {
this.touch();
}
touch(): void {}
}
class Counter extends Toucher {
count = 0;
label: string;
items: string[];
constructor() {
super();
this.label = "c";
this.items = [];
}
override touch(): void {
this.count++;
this.label += "x";
console.log(this.count, this.label, Number.isNaN(this.count));
try {
this.items.push("a");
} catch (error) {
console.log(error instanceof TypeError);
}
}
}
const c = new Counter();
c.touch();
console.log(c.count, c.label, c.items);
class Shower {
constructor() {
this.show();
}
show(): void {}
}
class Box<T> extends Shower {
label: string;
value: T;
constructor(value: T) {
super();
this.label = "box";
this.value = value;
}
override show(): void {
console.log("show", this.label, this.value);
}
}
new Box<number>(3).show();
new Box<string>("s").show();
type Ctor<T = {}> = new (...args: any[]) => T;
class Viewer {
constructor() {
this.show();
}
show(): void {}
}
function Labeled<B extends Ctor<Viewer>>(Parent: B) {
return class extends Parent {
label = "mixed";
override show(): void {
console.log("show", this.label);
}
};
}
const L = Labeled(Viewer);
new L().show();
interface Crash { loc: number; message: string }
class Setupper {
constructor() {
this.setup();
}
setup(): void {}
}
class Host extends Setupper {
crash: Crash | null;
name: string | null;
constructor(c: Crash | null) {
super();
this.crash = c;
this.name = "host";
}
override setup(): void {
console.log("setup", this.crash, this.name);
if (this.crash !== null) {
try {
console.log(this.crash.loc);
} catch (error) {
console.log(error instanceof TypeError);
}
}
}
report(): string {
if (this.crash) return `${this.crash.message} at ${this.crash.loc}`;
return this.name === null ? "none" : this.name.toUpperCase();
}
}
console.log(new Host({ loc: 3, message: "boom" }).report(), new Host(null).report());
@@ -0,0 +1,28 @@
// JavaScript class fields that the constructor creates after it calls a
// method, or after a base constructor that does, hold undefined until
// they are assigned, as in Node.
class Widget {
constructor(name) {
this.render();
this.name = name;
this.size = 3;
this.title = "widget";
this.parts = ["a"];
this.meta = { id: 1 };
}
render() {
console.log("render", this.name, this.title, this.size, this.parts, this.meta, this.size + 1, `${this.name}!`);
}
}
class Big extends Widget {
constructor() {
super("big");
this.extra = 2;
}
render() {
super.render();
console.log("big", this.extra);
}
}
new Widget("w").render();
new Big().render();
+8 -4
View File
@@ -1869,6 +1869,8 @@
"5842-qs-parse-null-prototype.ts": 1774,
"5843-qs-parse-null-prototype-js.mjs": 1559,
"5844-date-unknown-values.ts": 3969,
"5846-events-esm-default-max.ts": 2633,
"5847-events-cjs-default-max.cjs": 2823,
"5857-regexp-unicode-sets.ts": 3350,
"5858-disposable-stack.ts": 3050,
"5859-disposable-stack-js.js": 3100,
@@ -1899,6 +1901,8 @@
"5923-matchall-groups-js.js": 3970,
"5939-js-let-object-reassigned-wider.js": 1945,
"5942-local-class-observed-methods.ts": 5063,
"5943-fields-read-before-assignment.ts": 6856,
"5944-js-fields-read-before-assignment.js": 4247,
"5950-tuple-predicate-methods.ts": 5990,
"5951-result-never-widening.ts": 4710,
"5955-any-promise-methods.js": 3940,
@@ -1913,8 +1917,6 @@
"5990-worker-uncloneable.mjs": 3283,
"5991-worker-async-dispose.mjs": 5699,
"5992-worker-thread-name-lifetime.mjs": 2687,
"5846-events-esm-default-max.ts": 2633,
"5847-events-cjs-default-max.cjs": 2823,
"600-closures-basic.ts": 2349,
"601-closures-loops.ts": 1788,
"602-closures-identity-recursion.ts": 2372,
@@ -4569,6 +4571,8 @@
"5842-qs-parse-null-prototype.ts": 7077,
"5843-qs-parse-null-prototype-js.mjs": 5395,
"5844-date-unknown-values.ts": 4543,
"5846-events-esm-default-max.ts": 3102,
"5847-events-cjs-default-max.cjs": 3039,
"5857-regexp-unicode-sets.ts": 3900,
"5858-disposable-stack.ts": 3750,
"5859-disposable-stack-js.js": 3600,
@@ -4599,6 +4603,8 @@
"5923-matchall-groups-js.js": 6650,
"5939-js-let-object-reassigned-wider.js": 4436,
"5942-local-class-observed-methods.ts": 6212,
"5943-fields-read-before-assignment.ts": 72293,
"5944-js-fields-read-before-assignment.js": 40310,
"5950-tuple-predicate-methods.ts": 7790,
"5951-result-never-widening.ts": 6120,
"5955-any-promise-methods.js": 5120,
@@ -4613,8 +4619,6 @@
"5990-worker-uncloneable.mjs": 22432,
"5991-worker-async-dispose.mjs": 25768,
"5992-worker-thread-name-lifetime.mjs": 28932,
"5846-events-esm-default-max.ts": 3102,
"5847-events-cjs-default-max.cjs": 3039,
"600-closures-basic.ts": 2534,
"601-closures-loops.ts": 2297,
"602-closures-identity-recursion.ts": 2427,