TypeScript 5.1 RC permits different read and write contracts
Explicit getter and setter annotations can now describe unrelated types. The release candidate also corrects script emission and preserves the separate JSX tag contract introduced in the beta.
Reading a property and assigning to it are different operations, even when JavaScript uses the same name for both. An API may accept a string as input and return a structured view when read. TypeScript 5.1 can describe that relationship without requiring the read type to fit inside the write type.
The May 18 release candidate announcement retains the unrelated-accessor support introduced during the beta. Its implementation makes explicit annotations on both accessors the condition for opting into that relationship. This matters because inferred getter behavior still follows a different rule.
I appreciate type declarations that follow the semantics of a framework or platform API. TypeScript is useful to me largely because it makes web application code easier to navigate. Forcing an asymmetric operation into a symmetric property contract can make those declarations less informative than the underlying API.
Self-assignment was an assumption in the older rule
The release account explains the earlier restriction through box.value = box.value. If the getter type is a subtype of the setter type, a value read from the property can always be assigned back. That is a convenient invariant, but it is stronger than what every accessor API guarantees.
A property can normalize input, expose state through another representation or return an absence value before initialization. In those cases, accepting every possible read result as write input is not necessarily desirable. The read operation describes what the object can currently expose. The write operation describes what it permits callers to supply.
The announcement uses CSSStyleRule.style as motivation. A read yields a CSSStyleDeclaration, while assignment accepts a string. It explicitly says the built-in declaration has not yet been changed in this release candidate. The compiler capability should therefore be distinguished from the current declaration of that particular platform interface.
Both annotations make the distinction intentional
The implementing change requires the getter return type and setter parameter type to be explicit. Without a getter annotation, the setter annotation can supply the expected getter return type. This preserves an existing inference relationship instead of letting an accidentally different return silently establish a new contract.
Here is a complete class using the new relationship. It permits only strings as assignments while making the uninitialized read state visible.
class LabelBox {
private current: string | undefined;
set value(next: string) {
this.current = next;
}
get value(): string | undefined {
return this.current;
}
}
The getter includes undefined because construction alone does not establish a label. The setter excludes undefined because clearing the value is not part of this interface. Reading and then writing without narrowing can fail a type check, which is an accurate consequence of the declared operations.
The implementation proposal also shows completely unrelated types, such as a numeric getter and string setter. The important requirement is that the declarations describe an intended API. TypeScript is no longer using self-assignment as a universal proof that a getter and setter pair is sensible.
This shifts some responsibility to declaration authors. A property name can now connect operations whose types do not explain their relationship by assignability. Good API documentation still needs to explain what a write does and what a subsequent read represents.
Script emission is corrected during stabilization
The release candidate announcement identifies changes since the beta. Under isolatedModules, script files are no longer rewritten into modules. The same correction applies to transpileModule, whose behavior assumes isolated module processing.
This is a distinct concern from checking accessor types. A source file's status as a script or module affects how its declarations participate in execution. A transformation that changes that status can alter program behavior even when the source contains no new language feature.
The announcement also records corrected decorator init-hook behavior after changes to the proposal. That is relevant to adopters of the newer decorator contract. Stabilizing compiler output includes following the initialization order expected by the proposal, not merely accepting the decorator syntax.
I see these changes as reasons to read a release candidate announcement for its differences from the beta. Trying a feature early gives useful information, but a compiler preview's output behavior can still change before the stable release. The candidate describes the current contract to evaluate.
JSX eligibility remains a library-defined question
The release candidate includes JSX.ElementType, allowing a JSX library to define what can appear as a tag separately from the type of an element expression. That supports component return contracts broader than the element representation without weakening the latter.
Accessor types and JSX tags solve different problems, but both remove an unnecessary coupling. The getter result need not be valid setter input. A component return need not have exactly the same type as a JSX expression. Each change gives the relevant library declaration a more direct way to describe its runtime behavior.
The resulting precision depends on declarations as well as the compiler. An application cannot obtain a new platform property contract merely by upgrading while using unchanged types. The release candidate supplies the mechanism, and library authors determine which supported behavior those declarations expose to callers.