Skip to content

External module resolution for non-Typescript packages #2839

Closed
@poelstra

Description

@poelstra

Good progress is made on supporting the case of packages that carry their own typings in #2338.

That proposal doesn't currently deal with modules that do not carry their own typings, a.k.a. all modules that need typings from DefinitelyTyped.

This issue is to investigate possibilities of, and propose a (simple) solution for preventing problems when using (multiple versions of) external modules together.

TL;DR

  1. No longer use declare module "..." { } in typings for external modules
  2. Add an extra step to the resolution logic of External module resolution logic #2338 to look up this type of external typings

Example scenario

Suppose that both mylib and myotherlib export a function, that returns a value of a type defined in myutils.

Existing scenario

Likely, both mylib and myotherlib will each have a /// <reference path="./typings/tsd.d.ts" /> line, which again has a /// <reference path="./myutils/myutils.d.ts" /> line.

Problem

Both libs carry their own version of myutils.d.ts, which works fine as long as they are not used together.

However, when they are used together (as in this example), all the /// <reference ...> lines will (need to) be 'merged', which essentially means the declare module "myutils" { ... } from both mylib and myotherlib will end up in the same 'global module name namespace'.

This gives a compiler error, and even if the compiler would somehow allow it, would make it impossible to refer to either version of myutils.

Additionally, tools like dts-generator e.g. include the /// <reference> line inside of the declare blocks, which get ignored by the compiler. This leads to unknown-references when trying to use such a typing, and will typically be solved by adding e.g. a myutils/myutils.d.ts to myprogram/typings/tsd.d.ts, which will be wrong for one of the libs.

Solution

External modules have the nice property that there is no 'global namespace' (except the filesystem, maybe), and basically every reference is 'local'.

It makes sense to use the same referencing scheme that's used for resolving 'local' external modules (within a package) for 'normal' external modules (different npm packages).

Basically, the idea consists of:

  • No longer using declare module "..." { } in typings for external modules (to prevent the global namespace clash)
  • Adding an extra step to the resolution logic of External module resolution logic #2338 to look up this type of external typings (e.g. in typings/ folder)

Advantages

  • Multiple versions of a package can co-exist in the same program/library
  • /// <reference> lines are no longer needed in most cases (except probably for e.g. importing Mocha's it() and other 'true' globals)
  • One format to rule them all: external module typings are already generated without declare module "..." { } by tsc, so all 'sorts' of external module typings will look the same
  • Gradual upgrade path: the tsd.d.ts-way will still work for packages that do still use declare module "..." { } (although it will still have the name-clash problem etc.)

Disadvantages

  • Typings on e.g. DefinitelyTyped will (gradually) need to be replaced with their non-wrapped version
  • Extra lookup step in the compiler
  • Location of these external typings (a.k.a. include path) needs to be configured somewhere and/or sensible default value chosen

Example 'new-style' typings

Most hand-crafted typings will typically look like:

/// myutils.d.ts
export interface BarType {
    bar: number;
}
export declare function something(): BarType;

But just to make things more interesting, suppose the typings of myutils look like:

/// dist/bar.d.ts
export interface BarType {
    bar: number;
}

/// dist/foo.d.ts
import bar = require("./bar");
export declare function something(): bar.BarType;

Nothing special here, this is what tsc generates today.
(Note: dist/ may be because someone used CoffeeScript, not TypeScript, right? :))

Now, the typings dir on e.g. DefinitelyTyped could look like:

DefinitelyTyped/myutils/latest/dist/bar.d.ts
DefinitelyTyped/myutils/latest/dist/foo.d.ts
DefinitelyTyped/myutils/1.0/dist/bar.d.ts
DefinitelyTyped/myutils/1.0/dist/foo.d.ts

(Or maybe 2.0 instead of latest, ideas welcome.)

And both mylib and myotherlib would have:

typings/myutils/dist/bar.d.ts
typings/myutils/dist/foo.d.ts

Looking up .d.ts

Given e.g. import { something } from "myutils" the compiler could use myutils/package.json's main property to get to ./dist/foo.js, which in turn would resolve to dist/foo.d.ts.

Nice and simple: no need to have support from the authors of myutils, no /// <reference> lines.
And it even supports the import bar = require("myutils/dist/bar") case.

Open questions

  • Use one typings dir for both 'wrapped' and 'unwrapped' typings?
  • First do a lookup for external modules already declare'ed, e.g. through tsd.d.ts? If so, would probably make first bullet usable, nice for gradual upgrade path of DT typings.
  • Most DT typings will not want to mimic exact filesystem structure of package, using e.g. myutils/latest/index.d.ts will probably work out-of-the-box with external module resolution logic, but myutils/latest/myutils.d.ts or even myutils/myutils.d.ts may be easier for filename in editor tab?

Good idea? Bad idea?

Metadata

Metadata

Assignees

Labels

@typesRelates to working with .d.ts files (declaration/definition files) from DefinitelyTypedIn DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions