Search code examples
typescriptfirebasewebpacktsc

How does babel-loader / tsc compiler know NOT to import a package when it's being imported just for the types?


I'm currently implementing Typescript on my project.

And I've found out something curious.

You can see on the App.tsx file below that I had to import firebase from "firebase/app" in order to gain access to the firebase.app.App type.

App.tsx

import React from "react";
import firebase from "firebase/app";

interface App_PROPS {
  firebase: firebase.app.App | null;
};

firebase.initializeApp({});

const App: React.FC<App_PROPS> = () => {
  return(
    <div>App</div>
  )
};

export default App;

And I thought that this would become a bug in my project, because since I do SSR, that import CANNOT happen on my server code, because firebase/app is not meant to be used on the server. That is why the firebase package is passed passed down props, by the way.

But for my surprise, the package is not imported at all:

When I transpile it with babel using babel-loader (via webpack), this is what I get:

App.js - transpiled with babel

var _react = _interopRequireDefault(require("react"));

// (...) SOME OTHER CODE
// I WILL NOT POST THE FULL CODE HERE
// BUT "firebase/app" WAS NOT IMPORTED

Also when I transpile it using tsc src/App.tsx

App.js - transpiled with tsc

"use strict";
exports.__esModule = true;
var react_1 = require("react");
;
var App = function () {
    return (<div>App</div>);
};
exports["default"] = App;

// "firebase/app" WAS NOT IMPORTED

Could anybody explain to me what is happening in this situation? I'm glad the transpilers don't import the package, but why is that?

Is it because I'm only using the package inside an interface {} ? So, after transpiling for typescript that interface will be gone (since it cannot exist in JS) and the package will be dropped by some tree-shaking algorithm?

OBS: I've also tried the same code, but this time calling firebase.initializeApp(), which is a method from the firebase package. And in this case, the firebase/app package is imported in the transpiled versions of the App.tsx file, which makes sense.


Solution

  • It all comes down to Does this import been use as a value? Or just Type?

    I'll explain:

    Taken this for example:

    import firebase from "firebase/app";
    

    If you use firebase.initializeApp() - you use its value, the generated javascript from typescript compilation need to supply initalizeApp function and have it run in runtime - after compilation ends. This means it needs to have access to firebase import thus typescript compilation doesn't cut this import and later webpack transfer it to __webpack__require.

    On the other hand, if you use firebase: firebase.app.App | null; - you use its type. You are saying to typescript compiler: "While you type safe firebase property, make sure its of type firebase.app.App." - After this type safe check, there is no longer need for anything related to firebase.

    When typescript compiles a file, it checks if it uses only Types from a given import. If it does, it cuts that import.


    SideNote:

    In Typescript 3.8 import type keywords is introduce.

    It let you define

    import type firebase from "firebase/app";
    

    Which means this import can only be used for types. If you'll try firebase.initializeApp(), the build will fail. That will be classic for your scenario in case you are concern from future developers using this import as a value also.