August 31, 2017·6 min read·engineeringtypescriptmodulestooling

TypeScript 2.5 recognizes duplicate packages during type checking

TypeScript 2.5 redirects matching packages during Node-style resolution, adds control over symlinks, and permits catch clauses without unused bindings.

Two installed copies of a library can describe what appears to be the same class and still produce a type incompatibility. Private members make the distinction consequential because identical-looking declarations from different package locations do not necessarily represent the same class identity. TypeScript 2.5 addresses this problem by recognizing matching package names and versions during Node-style module resolution.

The August 31 announcement describes redirecting such packages to the top-most copy. The change is intended to resolve incompatibilities involving duplicated class declarations and can also avoid loading duplicate declaration files. It is a compiler resolution rule, so understanding its criteria matters more than assuming the dependency directory has physically changed.

I find this relevant to the framework ecosystem that makes TypeScript useful. A project can acquire dependencies through several other dependencies, and the resulting installation layout should not create an unexplained distinction between declarations meant to describe the same release. The checker needs a clear account of identity when it encounters those copies.

Matching names and versions drive redirection

Under the rule described in the announcement, the compiler examines the originating package's package.json. If its name and version match a package already encountered, TypeScript redirects to the top-most package. The rule applies when using the Node module-resolution strategy. Those are explicit conditions, rather than a general promise to merge any files with similar contents.

A package version is important because different releases can legitimately describe different interfaces. Equal names alone would not establish that the compiler should treat two installations as the same description. The announced rule uses both fields, making package metadata part of the checker's identity decision.

The correction concerns how the compiler consumes installed files. It does not instruct npm to rewrite the dependency tree or delete repeated packages. Runtime module loading remains a separate operation. A reader should therefore distinguish TypeScript selecting a common declaration from the package manager arranging which copies exist on disk.

That distinction also limits what the feature can claim. Matching metadata does not prove that arbitrary locally modified files have identical contents. The announcement defines the resolution behavior in terms of package name and version. It does not describe a content-hash comparison or a way to validate that a local package modification preserves the original release's declarations.

Private members explain why duplication hurts

The motivating case involves class declarations with private members. An application can encounter two declarations that expose the same visible operations, yet receive a compatibility error because the private portions come from separate declarations. The announcement explicitly connects deduplication to those otherwise identical package copies.

That makes this a type-identity problem as well as a resource-use problem. Avoiding another declaration load is useful, but the more immediate benefit is that the same package release can provide a shared source for the class description. A public API accepting that class no longer has to distinguish package copies merely because of where the compiler first found their declaration files.

The announcement also says avoiding duplicate .d.ts loads can reduce the compiler and language service's memory and runtime footprint. It provides no universal percentage, so the practical effect depends on how much duplication a project's dependency graph contains. A project with repeated matching declarations gives the rule more redundant work to remove than one without them.

The August 17 candidate announcement already described the same package-redirection design. That gives users testing the candidate a concrete behavior to inspect before the final release. The final announcement retains the name-and-version explanation and the private-member motivation, rather than presenting a different dependency-management mechanism.

TypeScript 2.5 also adds preserveSymlinks. When enabled, references resolve relative to a symbolic link's location instead of the location to which it points. The announcement includes imports and type-reference directives among the affected references. This changes which directory provides the context for subsequent resolution.

A symbolic link has both a visible path and a target path. Depending on which one a tool uses as the base, a reference can be resolved in a different surrounding directory. The flag therefore concerns the interpretation of the project layout, not whether the source uses a different import statement spelling.

The announcement says the option parallels Node's --preserve-symlinks behavior. It also points out that Webpack's resolve.symlinks setting has the opposite sense. TypeScript's preserveSymlinks: true corresponds to Webpack's resolve.symlinks: false. Remembering only that both tools have a symlink setting would miss that inversion.

I prefer configuration explanations that name the path each tool uses. A flag name alone can suggest the wrong relationship between two integrations. Here the useful question is whether dependencies are located relative to the link or its target, and the announcement answers it directly.

Package deduplication and symlink preservation remain distinct choices. The former recognizes package identity through metadata. The latter establishes the location from which references are resolved. A build can need both concepts, but enabling one should not be described as a substitute for understanding the other. Each changes a different part of the compiler's interpretation of installed source and declarations.

Catch can omit an unused binding

The release's catch change is smaller than a new exception-typing policy. A program can now write catch { ... } without declaring a variable for the caught value. The announcement's example reads a configuration file and falls back to defaults on failure, without inspecting the error object.

function parseOrDefault(text: string): any {
    try {
        return JSON.parse(text);
    } catch {
        return {};
    }
}

The example illustrates the omitted binding, not a recommendation that every parse failure should return an empty object. That decision belongs to the function's intended contract. The language change removes a name that the handler would not use. It does not make the handler distinguish malformed input from every other possible exception automatically.

This is why describing the feature as safer catch variables would be inaccurate. The variable can be absent altogether. A program that needs to inspect an error can retain a binding, and a program that needs a narrower fallback policy must implement that policy. The source simply no longer needs an unused identifier to express an intentionally unconditional handler.

JavaScript assertions remain claims by the author

JavaScript checking gains a JSDoc assertion form. The release shows /** @type {string} */ (expression), with parentheses around the expression. This allows a JavaScript author to tell the checker that an expression has a type more specific than the checker currently knows, while leaving the file in JavaScript syntax.

The announcement deliberately demonstrates that a successful assertion can still lead to a runtime error. It asserts a string type for an expression that may be a number, then calls a string method. The checker accepts the asserted narrower view in that example, but the operation has not converted the runtime value into a string.

Some incompatible assertions are still rejected. The announcement gives a numeric literal asserted directly as a string as an error. Assertions therefore have checking rules, while remaining different from runtime validation. They are a way to supply a claim within the type system's rules, not a mechanism for examining external data before use.

The union in the announcement's example explains why the initial diagnostic is useful. A value inferred as string | number does not always provide toUpperCase. The error describes a real possibility in the expression, even when the author prefers to proceed under a narrower assumption. A JSDoc assertion records that assumption for the checker while leaving the underlying possibility unchanged.

Keeping the parentheses in the assertion syntax also makes the scope visible. The comment applies to the enclosed expression, rather than declaring a new runtime conversion operation. This lets a JavaScript file express a local type claim without using TypeScript-only assertion syntax that its existing JavaScript execution pipeline would need to transform.

Refactoring support changes source structure

The release also introduces extract-function and extract-method refactorings. The announcement says Visual Studio Code support is coming in its next release and can be tried through Insiders. Availability in TypeScript's service is therefore distinct from availability in a particular stable editor build. The feature is described as new and still open to improvement.

Quick fixes address mistaken type syntax as well. One recognizes an attempt to refer to a property type through Foo.bar and suggests the indexed-access form Foo["bar"] where appropriate. That is a useful connection between a programmer's apparent intention and a supported type operation. The same release that makes package identity less dependent on duplicate file locations also helps the editor explain how an individual property's type should be written.