1

I'm currently writing a Typescript npm package with optional dependencies. Some of the types are only in effect if a package is installed and can be used.

type baseType = "foo" | "bar";

type additionalType = test | "baz";

"baz" only being possible if package xyz exists. So ideally I would be able to have the type adjust to if a package exists.

function doThing(): require("xyz") ? additionalTypes : baseType{
}

All that I have found for conditional typing uses extends which doesn't work when it's not based on user input but rather a third-party dependency.

CEbbinghaus
  • 86
  • 1
  • 10
  • 1
    I don't think it is possible, you will either need separate functions in 2 files for the case where your dependency exists, or write your own types that coincide with the ones of that package – Alex Chashin Sep 05 '21 at 09:13

1 Answers1

0

I think your only hope is if the optional dependency declares an ambient type (or modifies a built-in one), that you don’t have to import. Even if you can do it, you can’t import anything from it (at least at compile time), which means you’ll need to recreate any of its types you want to use (or work “blind” with any).

But if those limitations still work for you, you could try to use declaration merging on some ambient type.

For instance, if you know that the optional dependency has an ambient type like

node_modules/@types/optionalDependency/index.d.ts:

interface OptionalDependencyGlobal {
  someKnownProperty: unknown;
  /*…*/
}

Then you could write your own (empty) ambient type with the same name:

libs/types/checkOptionalDependency.d.ts:

interface OptionalDependencyGlobal {}

Then you could use conditional types based on

'someKnownProperty' extends keyof OptionalDependencyGlobal
  ? /* dependency present */
  : /* dependency absent */

Because you declared your own OptionalDependencyGlobal, you won’t get an error referencing it when the dependency is missing, and because you left it empty, keyof OptionalDependencyGlobal will be never, and 'someKnownProperty' extends never is false. Thanks to declaration merging, if the dependency does exist, then keyof OptionalDependencyGlobal will be 'someKnownProperty' | /* other properties */, and 'someKnownProperty' does extend that.

Note that you can pull the same kind of trick if the optional dependency uses its own declaration merging to modify a built-in type (or a type from a shared dependency). Consider:

node_modules/typescripts/lib/lib.dom.d.ts (built-in):

interface Window extends /*…*/ {
  // …
}

node_modules/@types/optionalDependency/index.d.ts:

interface Window {
  someKnownProperty: true;
}

Now you can use 'someKnownProperty' extends keyof Window just as we used it with OptionalDependencyGlobal above.

Either way, though, this is a brittle, hacky workaround, and if your optional dependency doesn’t do something like this, I don’t think you can detect it. Even when it works, it doesn’t let you import anything at compile time. I’d only consider this if I controlled both libraries, I think.

But I’m pretty confident there’s no direct mechanism for “optional import” that allows you to directly try an import and do something else if it’s missing; if the file specified in import is missing, you just get a compilation error. You could use require at runtime for optional imports (inside try/catch), but that won’t help at compile time and won’t help with typing.

KRyan
  • 7,308
  • 2
  • 40
  • 68