Oleg Mihailik
Oleg Mihailik

Reputation: 2590

TypeScript - conditional module import/export

TypeScripts abstracts away module imports/exports in sort of 'declarative' manner.

But what if I want to import or export something based on some runtime-computed condition?

The most common use case is sharing code between platforms like Node.js and Windows Script Host.

TypeScript's very own io.ts that abstracts input/output in TSC compiler manually hacks around the built-in TypeScript's very own module syntax. Is that the only way?

P.S. The problem with just sticking import fs = module("fs") into if statement is that TypeScript only allows import statements at a top level. Which means in WSH require("fs") will be executed and obviously failing, as require is undefined.

Upvotes: 51

Views: 68312

Answers (6)

mPrinC
mPrinC

Reputation: 9401

From TypeScript v2.4 you can use dynamic import to achieve conditional importing

An async example:

async function importModule(moduleName: string):Promise<any>{
    console.log("importing ", moduleName);
    const importedModule = await import(moduleName);
    console.log("\timported ...");
    return importedModule;
}

let moduleName:string = "module-a";
let importedModule = await importModule(moduleName);
console.log("importedModule", importedModule);

Upvotes: 40

Gabriel Arghire
Gabriel Arghire

Reputation: 2360

import { something } from '../here';
import { anything } from '../there';

export const conditionalExport =
  process.env.NODE_ENV === 'production' ? something : anything;

Inspiration from Andrew answer.

Upvotes: 0

cipherdragon
cipherdragon

Reputation: 373

Could not find a direct way to do conditional exports as conditional imports. But I found Andrew Faulkner's answer useful but I'm not happy with the limitations.

  1. You sadly have to set the import to 'any' to make the compiler happy.

I came up with a work around for the above limitation. Here's my steps.

  1. Write conditional exports as one object as in Andrew's answer.
  2. Import the exported object in another module.
  3. De-structure it.
  4. Define new constants by assigning all de-structured items giving the proper type.

Here's the example.

//CryptoUtil.ts

function encryptData(data : Buffer, key : Buffer) : Buffer {
    // My encryption mechanism.
    // I return a Buffer here.
}

function decryptData(data : Buffer, key : Buffer) : Buffer {
    // My decryption mechanism.
    // I return a Buffer here.
}

// Step 1
// Exporting things conditionally

export const _private = (process.env.NODE_ENV === "test") ? {
    __encryptData : encryptData,
    __decryptData : decryptData,
} : null;

Notice how I export encryptData as __encryptData instead of directly exporting as encryptData. I did that just because I can give the idea that __encryptData is a private function when de-structuring in the importer module. It's totally my preference.

Then when importing things...

// CryptoUtil.test.ts

// Step 2
// import the exported object.
import { _private } from "./CryptoUtil";

// Step 3. De-structuring.
const {
    __encryptData,
    __decryptData,
} = _private as any;
 
// Step 4. Define new constants giving proper type.

const _encryptData : (data : string, password : Buffer) => Buffer = __encryptData;
const _decryptData : (encryptedData : Buffer, password : Buffer) => Buffer = __decryptData;

// Now I can use _encryptData, _decryptData having the proper type.

Even though I propose this way to andrew's first limitation, my approach introduces a new limitation. That is, you have to define the type in two places. When you change the type of export function, it won't magically change the type of imported function. You've to change it manually.

Upvotes: 0

JBaron
JBaron

Reputation: 186

I agree that the fact that they can only have toplevel scope is suboptimal at best. Besides the issue you stated, it also means slower initial load times of software. For example within nodejs I now sometimes load a module in a function if that function is seldom used. So my application starts up quicker since it doesn't load that module yet.

And of course you could use require or AMD directly, but than you will miss some of the typing benefits.

I think however that the real problem lies in the fact that harmony/es6 defined modules to be toplevel and TS seems to be following that proposal. So not sure how much TS team can do without diverging from the standards.

Upvotes: 12

Fenton
Fenton

Reputation: 250842

There is a mechanism for dynamic imports in TypeScript, although the implementation differs based on the module kind.

The example below (for AMD) will conditionally load the module:

declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;

import * as ModuleAlias from './mod';

const someCondition = true;

if (someCondition) {
    require(["./mod"], (module: typeof ModuleAlias) => {
        console.log(module.go());
    });
}

The import statement at the top of the file is inert, and the actual loading of the module will not happen unless the condition if (someCondition) is true.

You can test this by changing someCondition and seeing the impact on your network tab, or you can look at the generated code... in the dynamic version, "./mod" does not appear in the define call. In the non dynamic one, it does.

With Dynamic Loading

define(["require", "exports"], function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    const someCondition = true;
    if (someCondition) {
        require(["./mod"], (module) => {
            console.log(module.go());
        });
    }
});

Without Dynamic Loading

define(["require", "exports", "./mod"], function (require, exports, ModuleAlias) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    const someCondition = true;
    if (someCondition) {
        console.log(ModuleAlias.go());
    }
});

Upvotes: 2

Andrew Faulkner
Andrew Faulkner

Reputation: 3930

I have a slightly clunky but very effective solution for this, particularly if you're using conditional import/export for unit testing.

Have an export that is always emitted, but make the contents vary based on a runtime value. E.g.:

// outputModule.ts
export const priv = (process.env.BUILD_MODE === 'test')
  ? { hydrateRecords, fillBlanks, extractHeaders }
  : null

Then in the consuming file, import the export, check that the imported value exists, and if it does, assign all the values you'd otherwise import stand-alone to a set of variables:

// importingModule.spec.ts
import { priv } from './outputModule';

const { hydrateRecords, fillBlanks, extractHeaders } = priv as any;
// these will exist if environment var BUILD_MODE==='test'

Limitations:

  1. You sadly have to set the import to 'any' to make the compiler happy.
  2. You need to check for whether or not specific imports are defined (but that comes with the territory).
  3. The importing file will expect the values to be defined. You will thus have to ensure importing files actually need the modules (which is fine if you're e.g. dealing with files only run during testing), or you'll have to define alternative values for cases where they don't actually get exported.

Still, it worked really well for me for my purposes, hopefully it works for you too. It's particularly useful for unit testing private methods.

Upvotes: 23

Related Questions