TypeScript 3.4 preserves build state and literal information
Persistent compiler state speeds later invocations, while const assertions keep literal values useful for discriminated unions and readonly tuples.
A watch process can reuse knowledge of a project after an edit, but that knowledge normally disappears when the process stops. TypeScript 3.4 adds persistent build information so a later compiler invocation can reuse relevant state. The release also addresses a different loss of information when inference widens a literal into a general type.
The March 29 announcement describes both changes. Incremental compilation saves project-graph information between builds. Const assertions tell inference to retain the specificity of a literal expression. They solve separate problems, but each makes information available at the point where the compiler would otherwise have discarded it.
The npm publication record dates the 3.4.1 package to March 29 as well. The announcement calls the feature release TypeScript 3.4. Those identities distinguish the release family described in the post from the exact package version available for installation.
I find both changes useful for web development. Compiler feedback needs to arrive soon enough to guide an edit, and inferred types need to preserve the distinctions an interface actually uses. Neither benefit requires more runtime machinery in the browser application.
Incremental information survives the process
Watch mode already has a dependency graph available while it runs. When a file changes, the compiler can use that graph to decide which files need checking and possibly new output. Reconstructing the same understanding for every independent build repeats work that an unchanged project does not necessarily require.
TypeScript 3.4's incremental setting tells the compiler to save information about the previous compilation. A subsequent invocation can read that information and determine a less expensive route through checking and emission. The feature consequently helps builds that do not share a continuously running watch process.
A configuration can choose an explicit location for the information file. This example keeps the cache separate from the directory containing emitted JavaScript.
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./buildcache/browser",
"outDir": "./lib"
},
"include": ["./src"]
}
The information file is compiler state. It does not participate in the emitted program's runtime behavior, and the announcement says it can be deleted safely. Without it, the compiler loses the opportunity to reuse that saved state and reconstructs what it needs.
That property makes the file different from an application asset. A browser does not need it to execute the output. Its purpose is to change the cost of producing that output and checking the source again.
The cache is private to the compiler version
Although the saved data is JSON, the announcement explicitly warns other tools not to treat its contents as a stable format. The stated policy is that one TypeScript version does not understand another version's build-information files. A consumer should not confuse a readable serialization with a supported interchange contract.
This matters to an external build system. It can preserve the file as an optimization input without interpreting its internal structure as application metadata. The compiler owns the meaning of that structure. Depending on individual JSON fields would introduce a coupling the release does not promise to maintain.
The announcement also warns that independent builds should not write to the same cache file. The explicit tsBuildInfoFile setting provides control over the location and name. Separate state files let distinct compiler configurations retain information without competing for one output.
Composite projects always produce build-information files under the new arrangement. With outFile, the default information-file name derives from the output filename. These defaults connect the cache to existing project organization, while the explicit path remains available when that default does not fit the build.
The reported Visual Studio Code timings distinguish an initial incremental build from later builds and distinguish changes to the public type contract from other edits. That distinction is more informative than a universal speedup claim. Writing state has a cost, and how much later work can be skipped depends on what changed.
Literal widening follows mutability
The type-inference change starts with a familiar JavaScript property. A variable or property can be mutable even when its initial value is a particular string. If TypeScript inferred only that initial literal everywhere, later assignments to other strings would fail unnecessarily.
The compiler therefore widens many literal values. A mutable variable initialized with one string can receive the general string type. That heuristic fits ordinary assignment, but it can discard information needed for a discriminated union.
Suppose an object contains a kind field whose exact value selects a particular shape of data. If inference widens that field to string, checking the kind no longer identifies one declared alternative. The program may need an annotation merely to preserve information that is visible in its initializer.
An explicit type annotation is one solution. It can tell the compiler the intended union and supply context for the initializer. The new assertion offers another solution when the data's literal structure itself should determine the precise type.
Const assertions preserve the literal expression
A const assertion uses as const after a suitable literal expression. It prevents widening within that expression, gives object-literal properties readonly types and makes array literals readonly tuples. Those consequences explain why the feature is useful beyond a declaration's choice of const or let.
A small discriminated collection shows the effect. Each kind names an operation with a different associated value, and ordinary branching selects the corresponding property.
const operations = [
{ kind: "read", path: "/report" },
{ kind: "retry", delay: 100 }
] as const;
for (const operation of operations) {
if (operation.kind === "read") {
console.log(operation.path);
} else {
console.log(operation.delay);
}
}
The kind values remain specific enough to distinguish the alternatives. Checking read permits access to the path belonging to that object. The other branch has the retry alternative and its delay. An author does not need to repeat the complete union definition merely to prevent those fields from widening.
The array also has a readonly tuple type. That says something about permitted operations through the typed reference. It does not add a runtime loop that recursively freezes the data. The assertion belongs to the compiler's interpretation of the expression.
Readonly does not imply deep runtime immutability
The announcement demonstrates an important limit with an object that contains an existing mutable array. A const assertion prevents replacing the object's contents property, but calling push on the referenced array remains allowed. The array arrived through another reference and retains its mutable type.
This distinguishes readonly properties from deep immutability. Preventing reassignment of one reference does not prevent every mutation reachable through that reference. A library that requires immutable data needs a contract that accounts for the nested values it accepts.
Const assertions also have placement restrictions. They apply immediately to suitable literal expressions. The announcement's conditional example cannot assert the whole conditional expression as const, but it can assert the literal values on its branches individually. That is a syntax and inference boundary, not an application-level freezing operation.
I prefer keeping these limits visible because const terminology can encourage broader assumptions than the type system provides. The useful result is precise inference and restricted writes through particular references. That is already valuable without attributing runtime guarantees the assertion does not create.
Readonly arrays become easier to express
The release adds readonly T[] syntax as a shorter form for readonly arrays and supports readonly tuples. A function can communicate that it will read an array without using its reference to add, remove or replace elements. The syntax makes that contract less cumbersome to write beside ordinary array shorthand.
Readonly mapped modifiers also gain meaningful behavior for arrays and tuples. Applying Readonly to an array now produces its readonly counterpart, and a mapped type that removes the modifier can recover a mutable container type. This makes generic utilities behave more consistently across object properties and array-like data.
The modifier remains restricted syntax for arrays and tuples. It is not a general-purpose operator that can be attached to every generic container. That restriction matters when a library exposes sets or other collection types with their own mutation interfaces.
The same release improves higher-order inference when generic functions return function types. The announcement shows composition preserving a generic type parameter in the resulting callable. Earlier inference could instead fall back to the empty object type and lose the relationship between the argument and the returned data. This is another case where retaining a relationship is more useful than requiring callers to reconstruct it through an annotation. It affects library combinators whose output must remain generic for later callers.
TypeScript 3.4 therefore preserves useful information at two different stages. Build files retain compiler knowledge between invocations, while const assertions retain the distinctions present in literal data. The first reduces repeated analysis work, and the second lets ordinary control flow use specific values that inference would otherwise generalize.