· 6 min read

The typed shadow

Type inference keeps human source concise, while coding agents need a compiler-generated semantic shadow containing inferred types, symbol targets, and diagnostics.

Shadow puppeteers hold six painted figures against a glowing orange screen during a grand shadow play at Wat Khanon Temple.
Paul-shy, CC BY-SA 4.0

An agent patch I reviewed added a type to code that already had one. The generic arguments looked clearer spelled out, and the agent, handed a file in which no annotation appeared, had reconstructed the missing fact from tokens. Type inference is peak programming for humans because the checker carries repetitive facts outside the source. An agent given only that source has to guess the rest.

users.ts

-const byId = new Map(
+const byId = new Map<string, User>(

Mira Voss

Hover on the removed line shows Map<UserId, User> — the checker had already proved the branded key. This annotation widens it to string, so the map now accepts values that never passed through the UserId constructor.

   users.map(user => [user.id, user] as const),
 );
The agent's patch as the review saw it, with the hover text the file never contained.

Why did adding a type make the program less typed? The annotation replaced a compiler result with a developer claim, and the claim was broader. A human sees the difference through editor tooling. A text-only agent sees two plausible generic arguments.

Humans should not write the hover text#

TypeScript's official inference documentation describes types flowing from initializers, return values, and surrounding context. The callback parameter in filter does not need an annotation because the collection already determines it. Repeating User there adds another place where a refactor can lie.

The older ideal is the principal type: infer the most general type justified by the expression rather than making the programmer restate it. Damas and Milner's principal type-schemes paper formalized that idea for ML. TypeScript is a much messier language, but the human benefit survives. Inference is compression with the proof still attached.

I haven't found a clean defect-rate study that isolates local annotation count from the rest of a language and team. My claim is narrower: when the compiler can derive a fact, copying that fact into editable source creates another version that can drift.

The human file and the compiler's reading of it are different views of the same program:

users.tsts
declare const userIdBrand: unique symbol;type UserId = string & {  readonly [userIdBrand]: true;};type Role = "admin" | "member";type User = {  id: UserId;  roles: ReadonlySet<Role>;};type UserIndex = {  byId: ReadonlyMap<UserId, User>;  admins: readonly User[];};export function indexUsers(  users: readonly User[],): UserIndex {  const byId = new Map(    users.map(user => [user.id, user] as const),  );  const admins = [...byId.values()].filter(    user => user.roles.has("admin"),  );  return { byId, admins };}

The agent lost the editor#

Human programmers do not actually work from raw source alone — a whole semantic layer sits beside the file all day, answering questions the raw text cannot.

Editor requestHuman in editorText-only agent
Hover
Completion
Go-to-definition
References
Signature help
Diagnostics
The semantic layer, request by request: available beside the file, absent from a plain file read.

The Language Server Protocol standardizes many of those requests, which means the semantic layer already has a transport. A coding agent that receives files through search and read tools can lose that entire layer.

A declaration file only partly repairs the loss. It describes a module's public surface, while the mistake above lived in a local inference, a contextual callback type, and the exact signature selected at one call site. TypeScript's compiler API already exposes the whole Program, its source files, semantic diagnostics, and declaration emit. The missing piece is an agent-sized projection of that state.

Recent agent work is moving in this direction. The compiler and language-server feedback paper argues that diagnostics, symbol resolution, type information, references, and refactoring preconditions are supervision signals, then packages language-server sessions into replayable analysis bundles. The editor's invisible half becomes data an agent can inspect and compare after an edit.

Generate a shadow, not a mirror#

A committed, fully annotated copy of every file would drift, consume context, and expose enormous implementation types that no reader chose as an interface. Generate the typed shadow from the exact compiler version, configuration, dependency graph, and source snapshot instead. Ask for the changed range plus its enclosing declarations.

semantic-shadow.tsts
import ts from "typescript";const program = ts.createProgram(files, options);const checker = program.getTypeChecker();function describe(node: ts.Node) {  const type = checker.getTypeAtLocation(node);  const symbol = checker.getSymbolAtLocation(node);  return {    span: [node.getStart(), node.getEnd()],    type: checker.typeToString(      type,      node,      ts.TypeFormatFlags.NoTruncation,    ),    symbol: symbol      ? checker.getFullyQualifiedName(symbol)      : null,  };}const diagnostics = ts.getPreEmitDiagnostics(program);

Each record needs a source span, the inferred type at that use site, the resolved symbol or overload, and current diagnostics. Flow-sensitive languages should report the narrowed type where the identifier is read, not only the declaration type. Long types can keep a stable handle so the agent expands them on demand instead of receiving a page of conditional-type algebra before every edit.

Snapshot identity matters too. A type computed against yesterday's lockfile can be more dangerous than no type, because it arrives with compiler authority attached. The semantic document should name the commit, compiler, options, and dependency state that produced it, then expire when any of those inputs change.

Let the checker steer the patch#

Types become more useful when they constrain generation rather than decorate the prompt. A 2025 type-constrained code generation study integrated inference into decoding for TypeScript and cut compilation errors by more than half while improving functional correctness. The model needed no extra prose explaining assignability because the type system removed invalid continuations from the search.

A practical agent can use a less invasive loop. Before editing, request the typed shadow for the symbols in the task and their callers. After applying a patch, ask the compiler for new diagnostics, changed public types, newly widened aliases, and the resolved targets of modified calls. The source remains the document humans review, while the checker supplies evidence for the agent's next move.

I wrote a first exporter that described every identifier in a forty-line component. It produced several screens of mapped-type sludge and buried the one narrowed union I wanted. The current version emits only changed symbols and gives long types a query handle. One conditional type still expands into a page, and I have not decided whether the agent should see that page or the alias that named it.