January 31, 2018·6 min read·engineeringtypescripttypesinitialization

TypeScript 2.7 checks when a class becomes ready to use

Strict property initialization checks constructor assignments, while definite assignment assertions describe initialization the compiler cannot prove. Unique symbol types add declaration-specific identity for property keys.

A class property declaration can promise a number even when construction never assigns one. Reading the declaration alone then gives a stronger impression than the runtime object warrants. TypeScript 2.7 adds a check for this mismatch, bringing initialization analysis to instance properties and requiring authors to account for properties left unset.

The January 31 announcement introduces strictPropertyInitialization alongside definite assignment assertions and unique symbol types. These changes concern different limits of structural type checking. A property's type describes the values it may hold, but the checker also needs to know whether a value exists. A symbol-keyed property needs an identity that a general symbol type cannot supply by itself.

I remain divided about TypeScript, but its ecosystem makes careful web development substantially more practical. Checks like this are part of that value. They identify a concrete disagreement between what a declaration says and what the program establishes, before a browser executes the resulting JavaScript.

Construction must establish the declared property

With strict property initialization enabled, each relevant instance property needs a property initializer or a definite assignment in the constructor body. A declaration alone does not satisfy the check.

class Endpoint {
    address: string;
    enabled = true;

    constructor(address: string) {
        this.address = address;
    }
}

The constructor establishes address, while the property initializer establishes enabled. If the first assignment disappears, the property declaration still says string but construction no longer supplies the value it promises. That is the case the new check is intended to reject.

The announcement also describes the alternative for a property legitimately allowed to remain unset. Its type can include undefined. That changes the contract visible to users of the class, allowing the type system to represent absence instead of pretending initialization always supplies a string or boolean.

This distinction matters in application models. A field representing an optional result differs from a required field accidentally omitted during construction. Both can be absent at runtime, but they call for different declarations. Widening the type is appropriate only when absence is part of the intended model.

The new flag participates in strict. Projects already enabling the broader setting therefore receive the new check on upgrade. The announcement explains how to disable this individual option explicitly, which makes it possible to distinguish an incremental adoption choice from abandoning the rest of strict checking.

That arrangement also means strict is a policy for enabling the compiler's strictness checks, not a frozen list that can never grow. A compiler update may expose previously accepted source. In this case the diagnostic has a specific subject, whether instance construction establishes a property whose declared type excludes the unset state.

An assertion records knowledge outside the analysis

Some initialization happens indirectly. A constructor may call a helper that assigns a field, or an external dependency-injection mechanism may establish it. TypeScript cannot necessarily prove the relationship through those operations. Version 2.7 adds a definite assignment assertion after the declared name.

class Endpoint {
    address!: string;

    constructor() {
        this.initialize();
    }

    initialize() {
        this.address = "localhost";
    }
}

The assertion communicates that the author takes responsibility for initialization despite the analysis's limit. It does not implement the assignment. Removing the call to initialize() would still remove the operation that supplies the address, regardless of what the assertion claims.

This is a deliberate escape from a conservative analysis. A type checker that accepted every possible indirect assignment without proof would lose the new check's value. A checker that offered no assertion would make certain established initialization patterns difficult to express. The syntax gives the exceptional claim a visible location at the declaration.

The feature also applies to variable declarations. The announcement shows a variable initialized through a separate function before use, with an assertion recording that relationship. This extends the same idea beyond class construction to deferred initialization in ordinary code.

I prefer an assertion that remains visible where the promise is made. It makes the source identify which initialization claims depend on the author rather than on the compiler's proof. That visibility is useful during review because the relevant question is whether the actual call ordering still establishes the value.

Symbols gain declaration-specific types

TypeScript 2.7 also tracks symbol-keyed properties more precisely. The announcement demonstrates constants created through Symbol() and used as computed property names. The compiler can associate each constant with the corresponding property's value type.

const countKey = Symbol("count");
const labelKey = Symbol("label");

const record = {
    [countKey]: 3,
    [labelKey]: "queued"
};

const count = record[countKey];
const label = record[labelKey];

With the appropriate symbol library available, count has type number and label has type string. Both keys are symbols, but treating them merely as interchangeable values of the broad symbol type would lose the connection between the key and the value it selects.

The new unique symbol type preserves identity tied to a declaration. It is a subtype of symbol, and declarations using it are restricted to places such as constants and readonly static properties. A reference to a particular unique symbol type uses typeof on its declaration.

This gives library authors a way to describe a protocol operation keyed by one exported symbol. A consumer can refer to that particular key and obtain the associated signature. The type system no longer has to collapse every symbol-named operation into one indistinguishable category.

The announcement also demonstrates symbols as discriminants for unions. Comparing a record's kind with the appropriate symbol constant can narrow the record to the corresponding variant. Identity and field shape then work together, allowing the compiler to determine which fields are available after the comparison.

These types do not create a new runtime identity mechanism. The program still creates JavaScript symbols. TypeScript adds a way for declarations and checked expressions to retain the identity information already relevant to the JavaScript operation.

Tuple length becomes part of compatibility

The TypeScript 2.7 release notes describe the tuple representation and the new narrowing rules in more detail. They also explain how optional properties participate in in checks, which matters when interpreting whether a branch proves that a property exists.

Another change in this release gives tuple types fixed numeric lengths. The announcement explains that earlier checking allowed a longer tuple to be assigned where a shorter one was expected when the element relationships permitted it. That follows a structural interpretation, but often disagrees with how programs use positional records.

Version 2.7 represents the tuple's arity through a numeric literal type for its length property. A two-element tuple consequently differs from a three-element tuple even when their leading element types match. The additional structural property allows the checker to distinguish the shapes.

This matters when a tuple represents a complete argument or result structure. A consumer expecting exactly two components should not silently receive a shape carrying a third component merely because the initial positions match. If a program needs a minimum-length array contract instead, the announcement shows how an array-based interface can express specified indices without fixing length to a literal.

Object-literal inference receives a related correction. When one branch produces an object with a field and another produces an empty object, the compiler previously could infer an empty object type for the combined result. Version 2.7 normalizes the alternatives into a union that preserves the field and represents its absence in the other branch. A later check can then recover useful information about the value.

That change helps the checker retain distinctions already present in ordinary JavaScript code. A program may intentionally omit a property to represent one variant. Losing the property during inference discards information that a later branch could use. Representing the missing member as an optional undefined property keeps the alternatives available to narrowing without asserting that both objects contain the same data.

Like property initialization, the tuple change can reject existing source. The reason is a more specific description of the intended object, either its readiness after construction or its positional size. The value comes from connecting the diagnostic to that contract instead of treating every new compiler error as an arbitrary inconvenience.

Import checking must agree with emitted JavaScript

The release also adds esModuleInterop for interoperability with CommonJS and other legacy module formats. The announcement explains that a namespace import should not be callable under ECMAScript module semantics, even though callable CommonJS exports had encouraged that pattern. Under the new option, those exports use a default import.

This option affects emitted code for legacy module targets, while the older allowSyntheticDefaultImports option concerned checking alone. That difference matters because accepting an import in the checker does not establish that the generated JavaScript obtains the intended value. TypeScript 2.7 provides an explicit interoperability policy, allowing the declaration-level expectation and emitted import behaviour to be considered together.