Keep a live Pick or Omit class view's identity when it flows into another structural type

A Pick or Omit view of a program class that holds the instance (its view
exposes a method) now carries the class's identity in the flow analysis,
and its values count as instances of that class. Assigning such a view
to another structural type keeps the instance, so reads through the
second type see the instance's later writes instead of a copy's.
This commit is contained in:
Chris Tate
2026-10-20 01:45:28 -05:00
parent 5ffc20450e
commit f4a7c4629d
2 changed files with 23 additions and 0 deletions
@@ -414,6 +414,11 @@ export class ClassProtocols {
for (const part of ts.constituentTypes(type)) add(part);
return;
}
// A Pick or Omit view's values are instances of the class it views.
if (this.pickOfProgramClass(type)) {
add(type.getAliasTypeArguments()[0]!);
return;
}
const symbol = type.getSymbol();
if (symbol === undefined) return;
if (this.isProgramClassInstance(type)) {
@@ -603,6 +608,8 @@ export class ClassProtocols {
return element !== undefined && this.carriesIdentity(element, marked);
}
if (this.isProgramClassInstance(type)) return true;
// A live Pick or Omit view of a program class holds the instance itself.
if (this.pickOfProgramClass(type) && this.usesCheckedIdentity(type)) return true;
const symbol = type.getSymbol();
if (symbol === undefined) return false;
if (marked.has(symbol)) return true;
@@ -37,3 +37,19 @@ void run({ client, name: "ctx" }).then(() => {
finish(client);
console.log(count(client), client.calls);
});
// A live view that flows into another structural type keeps the instance:
// reads through the second view see the instance's later writes.
interface Counter {
calls: number;
}
function counted(view: Pick<ApiClient, "request" | "calls">): Counter {
void view.request("/z");
return view;
}
const second = new ApiClient("https://k");
const counter = counted(second);
second.calls += 10;
const holder: { counter: Counter } = { counter: counted(second) };
second.calls = 100;
console.log("counter", counter.calls, holder.counter.calls, second.calls);