Skip to content

Compile SDK-declared data types, any-valued members and Pick views of classes - #1466

Open
cramforce wants to merge 5 commits into
mainfrom
opt/bc-sdk-types
Open

cramforce wants to merge 5 commits into
mainfrom
opt/bc-sdk-types

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

Service code often types its messages with declarations from SDKs and shared type packages. Several of those shapes were refused, and since a message type usually reaches a provider interface, one refused member blocked every function that handled the message. The diagnostic also named only the outermost member, so finding the offending one inside a 40-member record meant bisecting by hand.

any-valued dictionaries

type Dictionary<T = any> = { [key: string]: T };
interface Build { use: string; config?: Dictionary }
interface Upload { url: string; fields: { [key: string]: any } }

An index signature whose value type is any now holds checked-dynamic values, exactly as Record<string, unknown> does. This includes a generic dictionary alias used without a type argument.

any[] members

A record member that an interface, type literal or class declares as any[], also optional or nullable, is the checked-dynamic array, as unknown[] already maps. An object literal's inferred any[] member and all other any[] positions keep their existing rules, so programs that already compiled build the same code.

Data classes declared by types-only packages

// in a package's .d.ts, never constructed: the value arrives as JSON
export declare class PresignedPost {
  url: string;
  fields: PresignedPost.Fields;
}
export declare namespace PresignedPost {
  interface Fields { Policy: string; [key: string]: string }
}

A class that a package's declaration file declares with public instance fields only (no constructor, methods, accessors, index signatures, static, private or protected members, base classes or type parameters), optionally merged with a namespace of types, describes data. It now maps structurally, like an interface, so values from JSON.parse or object literals compile as records. Its class value, which has no implementation, stays refused.

Pick and Omit views of a class

interface Context { client: Pick<ApiClient, "request"> }
function finish(client: Pick<ApiClient, "request" | "close">) { client.close(); }

Pick<C, K> and Omit<C, K> over a program class, when the view exposes a method, are treated like an interface the class matches: values keep a live receiver, so calls through the view run on the instance and share its state. Field-only views keep their record representation.

Nested member paths in record diagnostics

values of type 'Provider' cannot be compiled: the record shape is supported, but its member 'poll().jobs[].handle' has type 'Promise<number> & { cancel(): void; }', which does not compile

The record-member diagnostic now follows the failure through function results (()), promise payloads and union arms, record members (.name) and array elements ([]) down to the member it comes from. A member that holds any is named itself.

Not included

Function types with any parameters ((reason?: any) => void) are handled by #1433.

Evidence

  • Corpus: 6412-any-valued-dictionaries.ts, 6413-sdk-declared-data-class (a types-only package) and 6414-pick-class-method-views.ts. All three match Node byte for byte in the plain and sanitized lanes.
  • Diagnostics: a new fixture, nested-member-blocker-path.ts. Existing snapshots are unchanged, because a path stops at the member that holds an any. diagnostics.test.ts: 200/200.
  • Zero-cost gate against main bf2afee: PASS, with all 2501 bench, self and corpus programs identical (IR, LLVM and release executables). The bench geomean changes by -0.23% and self-compile time by +1.31%, both within tolerance. A first run caught one corpus program whose object literal had an inferred any[] member; that is why the any[] rule is limited to declared members. The self-hosted compiler IR build exits 0 with no diagnostics. Frontend and IR unit tests: 1243/1243.
  • pnpm lint and pnpm format:check are clean, and pnpm test:ts7 --baselines-only passes, with 4 new baselines and no existing ones changed.
  • No compatibility rows change.

🤖 Generated with Claude Code

… classes

Message types from SDK declarations often use shapes scriptc refused:

- `{ [key: string]: any }` and `Dictionary<any>` (also a bare
  `Dictionary<T = any>`): the overflow values are checked-dynamic, like
  `Record<string, unknown>`'s.
- `any[]` members an interface, type literal or class declares, also
  optional or nullable: the checked-dynamic array, as `unknown[]` maps.
  An object literal's inferred `any[]` member keeps its existing story.
- A class a types-only package declares with public fields only (aws-sdk's
  `declare class PresignedPost { url: string; fields: Fields }`, with a
  merged namespace of types): it describes JSON data and is never
  constructed, so it maps structurally like an interface.
- `Pick<C, K>` and `Omit<C, K>` over a program class, when the view exposes
  a method: like an interface the class matches, it keeps a live receiver,
  so calls through the view run on the instance.

A record blocked by a member deep inside it now names that member's path
(`member 'poll().jobs[].handle' has type ...`) instead of the outermost
member only; a member holding `any` is named itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 20, 2026 5:34pm UTC

(declaration) =>
ts.isInterfaceDeclaration(declaration) &&
!declaration.getSourceFile().isDeclarationFile,
))

@vercel vercel Bot Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a structurally compatible class flows through a Pick or Omit view into another structural type, tracking only the named source class loses the actual instance's mutations and leaves downstream fields stale.

Fix on Vercel

@cramforce
cramforce requested a review from ctate October 20, 2026 05:13
…ther 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.
…Pick views

- A call that resolves to an overload signature of a generic function or
  method with an implementation (`load<T>(k, map: Mapper<T>): Promise<T>;
  load(k): Promise<Reply>; load<T>(k, map?) {...}`) monomorphizes the
  implementation under the bindings the overload's resolved signature
  proves; a type parameter the overload never mentions is unknown, and
  parameters the overload omits must be optional in the implementation.
  It was refused.
- `view.m(...)` where `view` is a `Pick`/`Omit` view of a program class
  and `m` is a generic (or generically overloaded) method extracts the
  instance from the view's live receiver and calls the method statically,
  instead of a runtime refusal reading the method through the untyped
  view.
- Binding inference pairs the arms of `T | Promise<T>` with an
  instantiated `R | Promise<R>`, and `unknown | Promise<unknown>` maps to
  the checked-dynamic value the checker reduces it to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +410 to +420
(_param, i) => expr.arguments.nodes[i] ?? expr,
);
returnType = lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(declSig));
} finally {
lowerer.typeParamBindings = previous;
lowerer.typeParamTsBindings = previousTs;
}
if (!returnType) return null;
// keyof-constrained parameters key their instance on the bound literal,
// as on the implementation-signature path.
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a generic overload implementation returns an out-of-range indexed read, the new overload path omits runtime return widening and throws a TypeError instead of returning undefined.

Fix on Vercel

This branch was successfully deployed

1 active deployment
Preview — 2fc57e82 Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants