TypeScript 5.8 checks the boundary between types and runtime syntax
TypeScript 5.8 adds erasableSyntaxOnly and aligns nodenext with newer Node module behavior. It also checks return-expression branches before any can conceal an incompatible value.
A runtime that strips TypeScript annotations needs a way to reject declarations that require executable code generation. Discovering that limitation only when a script runs makes the compiler's successful check less useful. TypeScript 5.8 gives the checker an explicit option for enforcing this syntax boundary.
The February 28 release announcement introduces erasableSyntaxOnly. It complements Node's direct execution of supported TypeScript files by detecting most TypeScript-specific constructs with runtime behavior. The release also updates module checking for newer Node behavior and strengthens checks inside conditional return expressions.
These changes address different ways that a checker can lose touch with the program's intended execution. A syntax declaration can assume an unavailable transformation. Module options can model an older runtime. A permissive branch type can conceal an incompatible return value.
Erasable syntax is an explicit source constraint
Node's 23.6 documentation describes a lightweight execution path that replaces type syntax with whitespace. It does not perform type checking or consult compiler configuration. Syntax that requires generated JavaScript is outside its default stripping mode.
TypeScript has long accepted such constructs because its own emitter can implement them. A parameter property, for example, asks the compiler to create a property initialization from a constructor declaration. An enum similarly needs a runtime representation.
The erasable-syntax implementation adds diagnostics for these declarations under the new option. The restriction is selected by configuration, so projects using the full compiler can continue to choose its broader syntax.
A checking-only configuration can express the intended relationship. The compiler checks imports and erasable declarations, while the runtime remains responsible for executing the accepted source.
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"noEmit": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true
}
}
The runtime still does not read this configuration. Instead, the compiler checks source under constraints chosen to match that runtime path. Separating those roles is useful because it states which tool enforces the contract.
The option does not mean that every accepted program runs in every JavaScript engine. Ordinary JavaScript syntax still needs support in the chosen runtime. The constraint concerns TypeScript constructs that cannot simply disappear.
Runtime behavior must remain visible in source
A parameter property illustrates the restriction. The following class asks the compiler to derive a property initialization from the parameter declaration even though its constructor body is empty.
class Entry {
constructor(public name: string) {}
}
The constructor contains an implicit assignment. Removing TypeScript-specific syntax would leave a parameter without establishing the instance field. Under erasableSyntaxOnly, the compiler reports the unsupported declaration.
The corresponding explicit form carries the same intended operation in ordinary JavaScript. Its constructor body includes the assignment that must remain after type information is removed.
class Entry {
name: string
constructor(name: string) {
this.name = name
}
}
The checker can remove the field's type annotation without removing the assignment. That source structure is more verbose, but it fits the local-erasure model.
The implementation discussion also examines import = aliases. An alias that transforms into a JavaScript declaration still requires replacement, even if the generated code is simple. Simplicity of output is different from erasure.
I am ambivalent about TypeScript, but I value conventions that make its execution requirements easier to inspect. The ecosystem can produce very good applications when its semantics are followed carefully. A compiler option that makes one runtime subset explicit reduces the amount of private knowledge needed to maintain those conventions.
Import syntax must survive checking correctly
The release recommends combining the new option with verbatimModuleSyntax. That addresses another transformation assumption, import elision. A stripping runtime needs an explicit distinction between imports used only for types and imports required as values.
A type-only import disappears from the executable program. A value import remains a dependency that the module loader must resolve. If a checker silently removes an unmarked import after analyzing its uses, the checked source and a simple runtime stripping pass can disagree.
The two flags therefore constrain different aspects of source. Erasable syntax rules exclude declarations that synthesize runtime behavior. Verbatim module syntax makes the import statements describe what the runtime should retain.
That relationship also gives code review a more concrete basis. A reviewer can inspect the import marker and know whether the declaration exists at runtime. Understanding the type graph is no longer necessary merely to infer whether the compiler plans to retain a dependency.
Node module modes model different runtimes
TypeScript 5.8 also supports require of ES modules under nodenext. Newer Node behavior allows a CommonJS module to require eligible ES modules, while top-level await remains a restriction on synchronous loading.
That affects package design. A library moving to ES modules has historically had to consider whether CommonJS consumers can load it. Supporting the runtime's newer interoperability behavior removes some checker errors that described an older limitation.
The release also introduces module: "node18" as a stable reference for that runtime generation. It disallows requiring ES modules, in contrast with the updated nodenext behavior. Choosing a mode therefore selects a compatibility model, not simply a spelling preference.
The distinction matters for reusable libraries supporting older users. An application controlling its Node version can select the corresponding behavior directly. A package with a broader support commitment needs its checking configuration to reflect the oldest environment it promises.
Import assertions provide another example. Under nodenext, TypeScript 5.8 rejects the older assert syntax in favor of import attributes using with. The stable Node 18 mode preserves a different historical contract. The compiler cannot choose the right policy without knowing the target environment.
Conditional return branches keep useful information
The return-checking work addresses a separate loss of information. A conditional expression combines the types of its branches. If one branch has type any, the combined result can simplify to any and conceal an incompatible value in the other branch.
TypeScript 5.8 checks each branch of a conditional directly inside a return statement against the function's declared return type. That lets it report an incompatible branch before the overall expression's permissive type hides it.
The following function has a declared numeric result but can select a string in one branch. The other branch deliberately has a permissive type to expose the checking problem.
declare const cached: any
function asNumber(useCache: boolean): number {
return useCache ? cached : "missing"
}
The string branch conflicts with the declared numeric return type. A check applied only after combining it with any loses that useful distinction. Checking branches separately retains the information long enough to diagnose the error.
The change has a defined scope. It is not a general removal of any from TypeScript, and it does not prove that the cached value is numeric. It catches the incompatible branch whose type remains available. The explicit return annotation supplies the expectation against which that branch can be checked.
The shipped release differs from its earlier preview
The announcement says broader work on conditional return types was pulled back after the beta. The narrower branch checks remain in the stable release. That distinction prevents a preview's design from being mistaken for the behavior available in 5.8.
Another configuration addition, libReplacement, controls lookup of replacement standard-library declarations through @typescript/lib-* packages. That lookup previously occurred even when a project did not use replacements. Disabling it avoids searching and watching for a package that the project does not intend to supply.
This is a distinct dependency decision from execution syntax. Ambient library declarations describe APIs available to the checker, such as browser interfaces. They do not install those APIs in a runtime. A project can therefore restrict its source to erasable syntax while still choosing an inappropriate library model for the environment where the code executes.
The release also preserves entity names used in computed class properties when emitting declaration files. The announcement warns that some resulting declarations may not be backward compatible with earlier compilers. A package's generated declarations are an input to its consumers' tools, so compatibility includes the compiler version reading those declarations as well as the runtime executing the JavaScript.
Compiler releases often contain both stronger diagnostics and performance work. This one avoids some array allocations in path normalization and reuses option-validation results when edits do not change project structure. Those changes affect the repeated work of loading and updating a program.
They do not change the meaning of the syntax constraint or module mode. Keeping those concerns separate helps explain an upgrade's effects. A build may become faster while also reporting an error that previously escaped checking.
My interest in this release is the clearer agreement it permits among source, checker, and runtime. The language remains capable of more than a stripping runtime implements. TypeScript 5.8 lets a project state the narrower syntax contract explicitly, select a Node compatibility model, and retain useful type information when checking a conditional return.