May 26, 2021·6 min read·engineeringtypescriptclassesgenericstyping

TypeScript 4.3 makes class contracts more explicit

TypeScript 4.3 adds override checks and separate accessor types. Its generic narrowing changes also distinguish the operations permitted by a constraint from the concrete type a function must return.

A subclass method can remain valid source after it stops overriding anything. A base-class rename leaves the old method behind, and the derived class silently changes its relationship to the hierarchy. TypeScript 4.3, released on May 26, adds an explicit override modifier to make that relationship checkable. The compiler can now reject a declared override when the corresponding base member disappears.

The release also separates the types accepted by a property setter from those returned by its getter. These changes make existing JavaScript behavior more expressible in TypeScript declarations. A method can state its inheritance dependency, and a coercing property can describe its input without weakening its output type.

I value these additions because TypeScript is useful when its static description matches what an API actually does. The web ecosystem depends on class and property interfaces with established behavior. Describing those interfaces accurately helps consumers without requiring the runtime implementation to change merely to accommodate its annotations.

Override intent survives a refactor

Without an explicit declaration, a same-named method in a derived class may be either an intentional override or a coincidental addition. The compiler has no statement of which relationship the author intended. Removing or renaming a base method can therefore leave an apparently valid derived method that is no longer used through the original path.

class Panel {
  show(): void {}
}

class LoggedPanel extends Panel {
  override show(): void {
    super.show()
  }
}

Here the modifier records a dependency on Panel.show. If the base method changes to a different name, the derived declaration no longer satisfies that requirement. The check is useful even when the method body would otherwise remain syntactically valid.

The noImplicitOverride option extends this discipline to overriding methods that lack the modifier. It reports an error when a derived method replaces a base method without declaring that intent. That covers accidental collisions as well as forgotten annotations. The modifier and the option therefore supply related checks with different triggers.

The release announcement credits Wenlu Wang's implementation and identifies earlier work by Paul Cody Johnston as a basis for discussion. This was a change to how the checker records and verifies intent. It does not create a new runtime dispatch mechanism. The class hierarchy still determines which implementation runs.

For library consumers, the relationship can cross a package boundary. A derived class may depend on a base member supplied by a dependency. Explicitly stating that relationship gives checking a way to expose an incompatible rename when the dependency's declarations change, even if the downstream method's own syntax remains valid.

A property can normalize what it accepts

A coercing setter may accept text while its getter returns a number. Earlier declarations could describe the property narrowly and reject valid assignments, or broadly and make reads less useful. Separate accessor types permit a broader write type alongside a precise read type.

interface Level {
  get value(): number
  set value(input: number | string)
}

An implementation must still perform any conversion. This declaration states what direct writes accept and what reads produce. It does not define how malformed text should be handled, or automatically install validation. Those remain decisions in the setter's body and the API's documented semantics.

In this release, the read type must be assignable to the write type. A number is therefore a valid value to assign back to this property. That requirement keeps self-assignment compatible while permitting additional input forms. It excludes some unrelated read and write combinations, even if a programmer can imagine a runtime object that implements them.

TypeScript also uses the reading type when relating same-named properties across types. The broader writing type matters for direct assignment. Ryan Cavanaugh's implementation discussion explains a related consequence for indexed access types. T[K] describes the read type, so a generic assignment helper built from it does not automatically preserve the setter's extra accepted inputs.

This is a useful boundary for abstractions. A transformed object type cannot promise that a copied value retains the original object's coercing setter behavior. A direct property assignment can therefore be valid while a generic helper using the property's read type rejects the same input. The apparent inconsistency follows from which part of the contract the helper actually describes.

A generic constraint does not name the caller's exact type

TypeScript 4.3 also improves narrowing for generic values. The release announcement motivates this with a function that accepts a set or array and returns the original collection. A union return type describes either possibility but loses the relationship between the input and output at a particular call.

A type parameter constrained by that union can preserve the relationship. The implementation then needs to inspect the value and use operations appropriate to the selected branch. Earlier checking struggled to expose those operations while retaining the original generic type for the return.

function sortIfArray<T, C extends Set<T> | T[]>(
  collection: C,
  compare: (left: T, right: T) => number
): C {
  if (collection instanceof Set) {
    return collection
  }
  collection.sort(compare)
  return collection
}

The example returns the original object. That matters because C can describe a more specific subtype than the union appearing in its constraint. Merely creating a new array of matching elements would not prove that it satisfies every possible C chosen by the caller.

The release explains that property access can use the narrowed constraint to determine available operations. In the array branch, that supplies sort. At a return, checking still needs to preserve the relationship to C. The compiler cannot generally replace the type parameter with its constraint and forget the caller's more specific choice.

This improvement supports implementations that already preserve the original value while performing operations justified by a runtime check. It does not establish that arbitrary transformations of a constrained value preserve its exact type. The return declaration remains an obligation on the implementation.

I prefer this example to a blanket claim that generic narrowing now works everywhere. It shows the two facts the checker needs at different points. The branch establishes available operations, and the retained object identity supports the declared return relationship.

Private names impose a runtime restriction

The release expands ECMAScript private names to methods and accessors, and permits private static members. These names provide runtime privacy, which is distinct from merely documenting that another part of the program should not access a member. The announcement demonstrates access from inside the defining class and rejection of outside access.

This gives class authors another implementation choice alongside the new static contracts. An override modifier states a relationship checked by TypeScript. A private name participates in JavaScript's runtime access rules. Treating every class-related addition as the same kind of guarantee would confuse their roles.

ConstructorParameters also gains support for abstract classes. The release connects that improvement to abstract construct signatures introduced in 4.2. A utility type can inspect the constructor's parameter tuple even though the class is not itself directly instantiable. Describing a constructor's arguments and authorizing construction are separate operations.

String patterns retain more useful inference

Template string types receive another related improvement. A function can describe a string with a fixed prefix and a variable portion, but checking an interpolated expression against that declaration requires the compiler to retain the expression's structure. TypeScript 4.3 uses a suitable contextual type to infer a template string type in more cases.

The release also improves comparisons between template string types. Its examples describe strings containing several numeric substitutions, with some positions fixed in a more specific type. The compiler can compare the corresponding pieces instead of giving up when both sides contain substitutions.

This lets a declaration express a family of structured strings without reducing every value to an unconstrained string. It still describes a static relationship. A value arriving from an external source needs the runtime checks required by that source and the application. The inferred pattern does not itself parse or validate incoming bytes.

Incremental compilation moves some work later

The release changes build bookkeeping as well as type checking. Its .tsbuildinfo format uses tables with numeric identifiers to avoid repeating full paths and similar information. That reduces stored duplication. The announcement reports reductions observed in particular projects, so those examples should not be treated as a universal size ratio.

It also defers some calculations in incremental and watch compilations until later builds need them. Initial compilation no longer has to perform all of that bookkeeping up front. Subsequent builds may pay some deferred cost while operating on a smaller affected collection of files and saving the resulting information.

The tradeoff concerns when the compiler computes information, rather than whether a project's source needs checking. Class contracts and generic narrowing affect what the checker can establish. The incremental changes affect how reusable build information is represented and calculated while developers repeatedly edit that source.