TypeScript 3.3 calls functions through their shared input types
Union calls can now use arguments accepted by every possible function. Composite watch builds also gain finer-grained reuse of compiler work.
Two functions can accept a common argument even when their parameter types differ. A variable that holds either function should permit that shared input while preserving uncertainty about the result. TypeScript 3.3 makes that reasoning available for more unions of callable types.
The January 31 announcement explains the earlier restriction. Calling a union worked when every constituent had one signature with identical parameters. Different parameter types could block the call even when a particular argument satisfied both. The new checker constructs a usable signature from their intersection.
That is a change in what the compiler can prove about existing JavaScript behavior. It does not dispatch between functions based on the argument at runtime. The variable already contains one function. Checking must establish that the call is valid regardless of which permitted function it contains.
I appreciate this kind of improvement because TypeScript is most useful to me when its type relationships describe actual interface behavior. A web application already has enough complexity in its framework and browser interactions. Rejecting a call whose shared input is evident adds a workaround without adding a useful runtime guarantee.
A union requires compatibility with every possibility
Suppose one handler accepts a refresh or reset action, while another accepts refresh or save. Calling an unknown member of that union with refresh is safe under both signatures. Reset is not, because the second handler does not accept it. Save has the corresponding problem with the first handler.
The following type-only example expresses that relationship.
type First = (action: "refresh" | "reset") => number;
type Second = (action: "refresh" | "save") => string;
declare const handler: First | Second;
const result = handler("refresh");
In 3.3, the call is accepted and the result has type number | string. The declaration describes a function supplied elsewhere. It is not an implementation to execute on its own. Its purpose is to isolate the relationship the checker now accepts.
The input restriction and output uncertainty move in different directions. An argument must work for every possible function, so the usable input is their intersection. A return value may come from either function, so the result remains a union. Treating the parameter as a union of all accepted inputs would incorrectly allow a call that only one possible function supports. In the example, passing reset could reach the second handler, whose declared contract excludes that action. The compiler has no evidence that a runtime selection has eliminated that possibility.
For this example, intersecting the literal unions leaves refresh. The other pairings require a string literal to be two different values at once, so they cannot supply an argument. The announcement demonstrates the same calculation with overlapping fruit and color names. Literal types make the surviving common member explicit.
The result still needs narrowing
Accepting the call does not identify which handler ran. A consumer using the result as a number must still establish that it is a number. The checker preserves the union because the successful input supplied no evidence about the function's return branch.
That limitation is necessary. Both possible functions accept refresh, and they return different types. A shared argument therefore cannot serve as a discriminator between them. The new permission is narrower than a promise that the compiler correlates every input with a unique output across a function union.
This distinction helps when designing an interface around callbacks. A union of callbacks says the stored callback may follow one contract or another. It does not create an overloaded function that implements every contract. The caller has to satisfy the uncertainty represented by the variable.
If an application needs a broader set of accepted inputs, the interface needs a corresponding implementation guarantee. An assertion that merely changes the apparent type would transfer that responsibility to the author. The 3.3 improvement avoids needing such an assertion for the genuinely shared case.
Overloads and generics limit the new rule
The announcement places explicit limits on the feature. At most one constituent of the union can have multiple overloads, and at most one can have a generic signature. This keeps the description more precise than saying all unions of callable values can now be invoked.
An array union is a familiar place to encounter the distinction. The release notes say methods such as map on number[] | string[] remain unavailable under the new rule because of their generic signatures. Having a method of the same name on each array type does not automatically provide a callable merged signature.
The notes also describe forEach as callable while warning about callback parameter inference with noImplicitAny. An explicit annotation for the callback's parameter can resolve that case. The ability to construct the outer call and the ability to contextually infer every callback parameter are separate checker capabilities.
For a union of arrays, an element annotation can itself be a union. The callback then narrows individual elements before using members specific to one type. This preserves uncertainty about the element while letting a common iteration operation proceed.
That is an important practical limit for library consumers. A release can accept more correct programs without solving every inference problem exposed by the new acceptance. The documented annotation is useful because it states the missing information directly instead of replacing it with an unrestricted type.
Composite builds have two levels of change
The same release improves --build --watch by using incremental file watching within composite projects. The announcement distinguishes selecting affected projects from selecting affected files inside a project. Those are separate opportunities to avoid repeating compiler work.
Composite projects already let build mode organize a larger codebase into smaller units. Before this change, a watch update could choose the right project but still rebuild that project in full. The incremental builder can now narrow checking and emission within the selected project according to changed files and dependency effects.
A build can therefore benefit even when the high-level dependency graph is unchanged. Selecting one affected project is still wasteful if almost all of its files remain semantically unaffected. The new behavior uses information already available to the incremental builder at the finer level.
The implementation discussion in pull request 29161 describes how the work fits the compiler. It records file versions using hashes of source text and retains builder state for comparing changes. It also describes releasing the program and its source files after the necessary checking and emission work.
That release of compiler objects matters because incremental state and the complete in-memory program are different resources. Retaining everything could reduce repeated work while increasing memory use. The implementation discussion explicitly addresses keeping useful comparison state without retaining the full expensive program indefinitely.
Diagnostics and emission impose ordering constraints
The pull request also describes a mismatch in assumptions about when diagnostics and emission occur. In build mode, semantic diagnostics can be requested before emission. The existing builder assumed changed files had already been emitted before that diagnostic operation.
The change records files that still need emission so the builder can handle this order. That is a concrete reason the optimization requires integration work instead of merely passing one extra flag. Reusing an incremental component requires preserving the sequence of operations its state represents.
The discussion notes that the first program creation still emits all files, as watch mode does. Incremental reuse benefits subsequent changes. Confusing an initial build with an update would produce an incorrect explanation of when the optimization removes work.
I consider build feedback part of whether a typed web codebase remains practical to maintain. The benefit here comes from avoiding unnecessary checking and emission after a change. It does not require an application author to weaken a type contract or restructure runtime behavior to obtain faster feedback.
The release announcement reports improvements in the team's tests, but those results should remain associated with the tested builds. The implementation explains the mechanism without requiring a promised percentage for every project. Dependency structure and the kind of edit determine how much work remains relevant.
More accepted calls preserve the original uncertainty
TypeScript 3.3's callable-union change makes a specific proof possible. An input accepted by every eligible constituent can be passed, and the result retains the union of possible return types. Restrictions around generics and overloads remain part of that contract.
The watch-build change likewise uses more precise information to avoid unnecessary work. It distinguishes an affected project from the files whose checking or output actually needs attention. Both changes improve the development experience by making existing relationships more accurately available to the compiler.