June 27, 2017·6 min read·engineeringtypescriptmodulesgenerics

TypeScript 2.4 makes module loading conditional

TypeScript 2.4 adds promise-based dynamic imports and string enums, while tightening generic compatibility and detecting option objects with no matching properties.

A module needed for one operation does not necessarily need to load when the application starts. TypeScript 2.4 adds import() expressions so source code can request a module at the point where it is needed. The expression returns a promise for the module, letting an application make loading conditional and letting supporting bundlers recognize a possible place to split output.

The June 27 release announcement presents this alongside string enums and stricter generic checks. Loading changes affect how the application arranges work at runtime. Stricter checking tests whether declarations faithfully describe relationships that the application already relies on. Together they make this a release with both new capabilities and compatibility changes that existing source may expose.

I find the loading feature relevant to browser applications because their useful functionality need not all be required during initial startup. TypeScript helps when its source-level model can express that timing decision and retain information about the module being loaded. The bundler and runtime still determine how the request is fulfilled.

Import returns a module asynchronously

The announcement's example loads a zip-file utility only when a function needs to create an archive. Its implementation awaits import() and then calls an exported operation on the resulting module object. A promise can also be consumed with .then(), so the new expression does not require every caller to adopt an async function.

A static import describes a dependency outside that local control flow. A dynamic import can appear where a condition or operation makes the need for the module known. That gives the application an explicit expression for delayed loading. It also gives tools a recognizable module request without requiring them to infer the intention from an unrelated custom loader function.

The announcement recommends considering the esnext module target with bundlers that split code using import expressions. It explicitly says the feature does not work with the es2015 module target, because the syntax is beyond that standard's module facilities. Selecting a target is therefore part of preserving the expression for the tool expected to handle it.

The distinction between a request and a split bundle is important. TypeScript source can express when to request a module, while a supporting bundler decides how to package it. The announcement says many bundlers support splitting at these expressions. It does not promise that every build configuration produces the same number of files or that every delayed module makes startup faster.

This fits a broader separation visible in the TypeScript 2.3 announcement. That release let projects choose checking and output behavior independently for JavaScript. Here the source also needs to cooperate with another build stage. Understanding which tool consumes the emitted syntax remains part of understanding the application pipeline.

String enums can describe an existing representation

TypeScript 2.4 allows enum members with string initializers. The release notes explain two practical benefits, more readable debugging values and the ability to describe systems that already represent states with strings. The value in a log or exchanged message can consequently carry a meaningful name without relying on the reader to interpret a numeric member.

enum ShapeKind {
    Circle = "circle",
    Square = "square"
}

interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}

interface Square {
    kind: ShapeKind.Square;
    sideLength: number;
}

type Shape = Circle | Square;

The announcement uses this pattern for discriminated unions. Each interface's kind property identifies its variant, so the enum member participates in the static description as well as providing a runtime value. The associated properties can differ between variants without making every possible shape contain every property.

The strings are explicitly chosen representation values. Changing one can therefore affect code or systems that consume that representation, even if the enum member's source name remains familiar. Type checking explains which values belong to the model, but an application still owns the contract with external data. Adding enum syntax does not automatically validate a string received over a network.

I see this as a useful place for TypeScript to reflect the application domain without obscuring it. A named member and its readable value are easier to connect than a numeric code whose meaning is elsewhere. The declaration remains valuable only when it matches the representation the rest of the system actually uses.

Return types contribute to inference

Generic inference now uses return-type information in contexts where it previously did not. The announcement gives a function that transforms a callback into an array-mapping function. Annotating the resulting function as accepting strings and returning numbers now helps infer that the callback's parameter is a string.

That reduces sensitivity to where the author writes an annotation. Information on the outer result can constrain an inner generic call instead of forcing a redundant annotation inside the callback. The improvement is concrete because the earlier default could be {}, which does not retain the intended string information.

Better inference can also reject source that previously passed. The announcement assigns a constructed promise to Promise<string> and calls its resolver with 10. The new inference uses the expected promise type to identify the incompatible result. A newly reported error can therefore indicate that the checker has finally used an existing annotation, rather than that the application deliberately changed its declared contract.

This is a recurring tradeoff in type-checker maintenance. More information makes some previously uncertain operations valid and reveals other operations as invalid. Treating every new diagnostic as an arbitrary compatibility break misses the relationship the original annotation was supposed to express.

Generic function compatibility becomes stricter

TypeScript now tries to unify type parameters when comparing two function types that each have one call signature. The announcement contrasts a function with two independent type parameters and a function requiring both arguments to share one type parameter. Those signatures make different promises even though each appears broadly generic.

A function accepting independently typed arguments must support combinations that a same-type function need not support. Assigning the latter to the former's type can therefore claim more flexibility than the implementation provides. The release rejects that direction while permitting the assignment that preserves the required contract.

The announcement offers noStrictGenericChecks as a temporary workaround for some resulting errors. This option disables the new checking behavior without establishing that the signatures are semantically equivalent. A declaration author needs to determine which relationship the implementation actually supports. If two argument types must agree, the declaration should preserve that requirement.

Callback parameter checking also tightens. The release connects the previous bivariant behavior to incorrect relationships among Promise and Observable-like types. Its Mappable<T> example rejects assigning a container that can map strings or numbers to one described as mapping numbers only. The callback's direction of use makes those containers' promises different.

This is a specific improvement to callback checking in 2.4. It should not be read as a claim that every possible function-variance issue in the language has been resolved. The announcement gives the particular structural relationship being corrected and notes that existing code can be affected. That is enough to explain why a library's generic declarations may need revision when the compiler becomes more accurate about them.

Optional-only types need a point of agreement

The release calls a type containing only optional properties a weak type. An options object fits that definition when callers may omit every setting. Without an additional check, an unrelated object can appear compatible simply because none of the declared properties is required. That can conceal a typo in the setting names or an object intended for a different API.

TypeScript 2.4 rejects an assignment when the supplied type has no property overlap with the weak type. The announcement's options accept data, timeout, and maxRetries, while the supplied object uses payload and retryOnFail. Such a mismatch suggests the caller used the wrong names. Checking can identify that problem even though every actual option is optional.

This is not a requirement to specify every option. An omitted setting remains omitted, and an object with an appropriate shared property presents a different case from one with no relationship at all. The new rule identifies the lack of any evidence connecting the supplied object's properties to the options interface.

The announcement suggests changing the declaration when the broader input really is intended, including adding an index signature or an always-present property. It also mentions assertions for callers certain about compatibility. Each choice has a different meaning. A revised interface describes the accepted data more faithfully, while an assertion asks the checker to accept a claim supplied by the programmer. The practical improvement is that an unrelated options object now requires attention at the point where it enters the API.