July 1, 2021·4 min read·engineeringtypescriptnarrowingoptional-properties

TypeScript 4.4 beta retains the meaning of named conditions

TypeScript 4.4 beta follows type guards through constant aliases. Its exact optional-property option also distinguishes absent keys from explicitly undefined values.

Giving a condition a name can improve readability while making a program harder for a type checker to understand. Earlier TypeScript versions could narrow a value inside an inline typeof check but lose the relationship when that check moved into a constant. The TypeScript 4.4 beta announced on July 1 preserves that information in supported cases. A small refactor no longer has to remove the checker's understanding of the branch.

The beta also introduces exactOptionalPropertyTypes. That option addresses a different loss of information, the distinction between a missing property and one whose value is explicitly undefined. Both changes matter where an apparently equivalent spelling conceals a distinction the program depends on.

I prefer code that names a meaningful condition without requiring an assertion to compensate for the refactor. TypeScript's usefulness in web applications depends partly on whether ordinary, legible JavaScript remains straightforward to check. Preserving a guard through a constant supports that goal without inventing a separate runtime operation.

The boolean retains a relationship to its subject

The beta recognizes a constant initialized from a type guard and can use that guard when the constant is tested. The announcement describes supported subjects including constants, readonly properties and unmodified parameters. Those restrictions matter because a condition about an earlier value cannot justify arbitrary assumptions after that value changes.

function label(value: unknown): string {
  const isText = typeof value === "string"
  if (isText) {
    return value.toUpperCase()
  }
  return "untitled"
}

The branch uses the original typeof relationship through isText. The compiler is not merely learning that the boolean is true. It follows the condition that produced the boolean and uses it to narrow value at the access.

The same approach applies to discriminated unions. A constant holding a comparison against an object's discriminant can establish which variant is available in a later branch. Composed conditions can also carry information transitively, such as a constant combining a string check and a number check.

The announcement states that the analysis has a cutoff. It does not promise unlimited symbolic reasoning through arbitrary chains of boolean expressions. The useful change is support for common aliases while keeping the checker's work bounded.

The implementation discussion also identifies an unsupported related pattern. Destructuring a discriminant and a payload into separate variables does not establish a coupling between those variables for this feature. That limitation prevents an overly broad reading of the announcement. Following a named guard is not the same as preserving every relationship after an object has been split apart.

Absence differs from a stored value

An optional property such as timeout?: number allows an object to omit the key. Under earlier behavior, TypeScript also treated its type as accepting an explicitly stored undefined. Ordinary property reads can return undefined in either case, which makes the two objects appear interchangeable in simple expressions.

Their keys can still differ. The beta announcement identifies Object.keys, object spread and Object.assign among the operations that observe property presence. A configuration object that omits a setting and one that supplies the setting with an undefined value can therefore affect a merge differently.

interface Settings {
  timeout?: number
}

const omitted: Settings = {}
const configured: Settings = { timeout: 30 }

// Rejected with exactOptionalPropertyTypes enabled.
const explicitUndefined: Settings = { timeout: undefined }

With exactOptionalPropertyTypes, the declaration does not implicitly add undefined to the property's accepted value type. An API that intentionally accepts an explicit undefined value can state that in its declaration. The question becomes part of the contract, rather than an automatic consequence of the optional marker.

The option requires strictNullChecks and is not included in the strict family. A project must enable it explicitly. That choice matters for compatibility because existing declarations and object construction code may rely on assigning undefined to optional fields.

For an interface that accepts both forms intentionally, timeout?: number | undefined preserves that permission explicitly. Its longer spelling records a decision about accepted values that the shorter optional declaration no longer makes automatically under this option.

More precise checking can reduce assertions

These changes affect different points in a program. Guard aliases improve the information available after a branch condition succeeds. Exact optional properties improve the information recorded when an object is created or a property is assigned. Neither changes the runtime behavior of reading a missing property.

A useful consequence is that declarations can reflect whether presence itself carries meaning. If a merge interprets an explicitly present key as an instruction, the value type should describe that instruction. Treating optional syntax as an automatic permission to supply undefined can obscure that distinction.

I prefer the explicit option for interfaces where omission has operational meaning. It gives the compiler a way to reject a different object shape before that shape reaches a merge. The named-condition change then helps code use established checks without repeating their expressions or adding assertions.

The beta makes both behaviors available for evaluation. A named type guard can retain its connection to the checked value, while an optional property can retain the difference between absence and an explicitly supplied value. Each improves the static description of decisions already present in ordinary JavaScript code.