Use type definitions from .d.ts file without importing

Typescript 3.8 adds this feature with the import type {} from '...' syntax:

import type only imports declarations to be used for type annotations and declarations. It always gets fully erased, so there’s no remnant of it at runtime. Similarly, export type only provides an export that can be used for type contexts, and is also erased from TypeScript’s output.

It’s important to note that classes have a value at runtime and a type at design-time, and the use is context-sensitive. When using import type to import a class, you can’t do things like extend from it.

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html


My hope here is to provide some digestible context for what the compiler is complaining about, why, and how to fix it. Unfortunately, the module-importing restrictions you described may be a different problem for you to tackle (I think AMD and SystemJS would be your only options with --outFile, but see my transpilation note later on.)

You referenced the triple-slash directive in your question

/// <reference types="..."

which is the directive for telling the compiler about type dependencies inside TS declaration files (.d.ts) -- not quite what's needed, since we're writing TS and not a declaration file. We want to include a missing declaration file that already exists somewhere. Note that TypeScript tries really, really hard to resolve and include type declaration files automatically for the developer, but libraries like color-js which don't specify a types location in their package.json nor use a typings or @types convention, the compiler just isn't able to find it.

You likely don't want to ditch your intellisense benefits by declaring global any variables to get around this issue. There is a criminally under-documented object called paths that we can add to our tsconfig.json compiler options, which is how we inform the compiler of additional places to look for types instead of modifying auto-loading behavior like the typesRoot or types options. Here's an example tsconfig.json file that seems to work well:

{
    "compilerOptions": {
        "module": "system",
        "target": "es5",
        "allowJs": true,
        "outFile": "./dist/main.js",
        "rootDir": "./src",
        "baseUrl": "./",
        "paths": {
            "*": ["color-js", "./node_modules/color-js/color.d.ts"]
        }
    },
    "exclude": ["dist"]
}

What this paths entry tells the compiler is to resolve any imports for the string "color-js", instead of resolving the module to the JS file, grab color.d.ts as the desired module instead from the specified path.

Here's what an example TS (or JS!) file might look like:

import Color from "color-js";

let colorManager = Color();

let twenty = colorManager.setRed(20).getRed();

While this solution uses the import keyword, one thought is that since we're transpiling to an outfile (read: it's a single file!) using the above tsconfig.json, we won't have to worry about the import keyword making it into the resulting main.js.

Let's say this configuration doesn't meet your use case, and you still find yourself searching for a way to use the type declarations without importing the module. If there is truly no other option for you, using a hack like the any-typed variable is the last resort.

Most declaration files are all-or-nothing in this regard, because what's actually getting exported from the .d.ts file (color-js's included) behind the scenes is a function that can build instances of Color. That's what the let colorManager = Color(); line is all about, we're utilizing the exported builder in addition to the type information about what the function builds. As you noticed, at run-time we may still be fine, but as far as the compiler is concerned if we can't import the typed function and call it, the build is dead in the water.