Check out a free preview of the full Intermediate TypeScript, v2 course

The "ES Module Imports & Exports" Lesson is part of the full, Intermediate TypeScript, v2 course featured in this preview video. Here's what you'd learn in this lesson:

Mike dives into ES module imports and exports, exploring examples of named imports and exports, emphasizing the benefits of organizing code into multiple files. The example covers named imports involving consuming named exports, as well as consuming default imports.

Preview
Close

Transcript from the "ES Module Imports & Exports" Lesson

[00:00:00]
>> Let's first talk about ES module imports and exports. And as usual, I'm gonna switch over to VS code. It's exactly the same examples that you see on the website. So if you prefer to read, you can follow along, but I like to show you hands on how this works so I can poke at things and screw them up and show you how they're screwed up and then fix them.

[00:00:19]
Sometimes that helps to land some of these points. So without further delay, let's look at index.ts here. So whenever I have a folder like this, it's an indication that we benefit from having multiple files here. We're talking about imports and exports and why fake it in a file?

[00:00:38]
We can have multiple files and see how this actually works. Index is where we're gonna begin. That's sort of the entry point. So let's talk about ES module imports and exports. What we have here are a bunch of examples of named imports, a bunch of different varieties of things that we can do.

[00:00:56]
First off, we have some named imports here that involve consuming named exports. So that would be something like this on the export side, right? Export class FruitBasket, something like that. And then you'd consume it with the named import. Here we're consuming a default import. So if you did export default class FruitBasket, I have multiple default exports.

[00:01:23]
You know what? There's something at the bottom. I can fix that. I always put this thing at the bottom. Typescript regards each file as a module. Export default class FruitBasket. This is the default export. This would be the matching import on the other side. You put the file name here obviously, and then whatever locally you want to call that name, with default imports you get to choose your name.

[00:01:44]
You can make this Grape. It doesn't need to match the name of the class as defined in this key we file. That's your local name. It's almost like a variable. Not true for this syntax, but you could rename it you could do this. Come on tooltip overload here.

[00:02:08]
Something like that Grapes, so this is an alias where you can say, all right, in this file I'm importing from it's exported as Blueberry but within my file I want to call it Grapes, which you have that option. And I only tend to do that when there's a collision between things.

[00:02:30]
Generic things like builder, or form, or error. Usually if you can have a couple of those playing together side by side and you need some disambiguation between them. All right, so we talked about a named export, that's where we don't have the word default here. So this would be exported as makeFruitSalad.

[00:02:51]
This would be a default export. There are also these things called re-exports. And you'll see this a lot in libraries that sort of have a single entry point, right, where you're just using this library right from its root. It's like one package that's the name of its package.

[00:03:05]
And if you look into those projects, you might see that there's an index.ts file that will effectively have no interesting code in it, it'll just strictly be a bunch of re-exports. What we're saying here is, I want to import lime and lemon from the citrus module. And I want to export them as if they're my exports, that's a re-export.

[00:03:32]
What we have here, we would call this a namespace export. And what we're effectively saying here is there's a bunch of stuff on berries, one or more things that berries, the file exports I want to re-export it all. Every single thing that's there. Take the whole thing, but I want it to be exported from this module as berries.

[00:03:58]
Here we go back to kind of a namespace type thing where you can have a bunch of different things hanging out there. And if we look at berries in this case, we can see, there are some re-exports there, all the way down. So we've got like a Raspberry, which has a pick Raspberries and a Raspberry class.

[00:04:16]
So all of that would end up kinda coming through all the way. You can re-export as many times as you want. TypeScript knows how to deal with this. So if you use like jump to definition, or anything that would let you, when you're consuming something from berries, you'd say, jump to definition and it would go to actually where it's declared.

[00:04:37]
It would just sort of think of those re-exports is almost like 302 redirects. That'll just capturable traverses through those and then it'll send you on your way to where you actually wanna go All right, here's namespace imports. This will be important as we look at CJS modules and CJS interrupt.

[00:05:02]
So here, we can import allBerries, and you can see here, there's our Strawberry, Blueberry, and Raspberry. We kind of saw that in the file, there were clearly re-exports within those. But if you wanna look at what allBerries includes, it's the three classes. And we went into the Raspberry file, we saw there was a pickRaspberries function.

[00:05:21]
It's a separate named export. And that's here too. It's star.star, send it all through, right? That's what a namespace export does. And you often do this, it avoids collisions. You could also say this. But things might bump into each other. You might have things in different files that are of the same name and it's gonna be a little bit confusing.

[00:05:55]
It's gonna be a little hard to manage. So, it's more common to do something like this. If you're creating a library with a bunch of different things available, where do you wanna consume the library as a whole? Importing types. So in this case, let's say that we do have a Raspberry thing in scope.

[00:06:13]
We can click on this and see where we're gonna go right to the definition. Where did we import Raspberry? It's from up here. So, here we're using Raspberry in the type position and down here we're using it as a value. We're actually instantiating a Raspberry. So, we have this Raspberry identifier in scope.

[00:06:37]
Remember that term that we coined when we were dealing with declaration merging, right? It's an importable, exportable thing, and it's a class. And remember, we talked about how classes can be used as a type and a value. So we can have something like this. We can use it as a type, and we can use it as a value.

[00:06:58]
And in this case, we need both to run lines 27 and 28 successfully. We need it to be usable in both of those forms. But let's say we didn't care about this. We didn't have that in our code. We're only consuming it as a type. Well, there's a different way to do this and we're switching to Strawberry now because I don't think that was something we imported up top.

[00:07:24]
It was just Blueberry and Raspberry. So we can import type Strawberry, and look how we can't actually use Strawberry to create an instance we don't have the value in this scope. We can use it as a type, but what's the error we get here, cannot be used as value cuz it was imported using import type.

[00:07:46]
Now, Typescript is actually quite smart about this. So if we just had a piece of code like this, where Raspberry doesn't happen to be used as a value, types of people kind of erase the import of Raspberry when it compiles down to JavaScript. It will be able to detect that although we imported this conventionally, that we didn't have this here, we just had import Raspberry that was up at the top here, there it is.

[00:08:21]
TypeScript will be smart enough cuz it does a holistic analysis of our program and it'll say, you never actually instantiated Raspberry. You just use the interface side of the class. And so we won't include that module in your bundle. There will be no resultant import. Bug Workbench. I think this is the thing that will understand multiple files, and indeed it does.

[00:08:48]
If you ever wanna go to this mode here, it's typescriptlang.org/dev/bug-workbench. This is what gives you some of the special syntax that I use. Here where I've got file name and sometimes I have a little like caret. This is a little aside, but I have little symbols that make those tool tips pop up sometimes not in this page, but that's, that's kind of what we're working with here.

[00:09:17]
So we've talked about importing types. We talked about type-only imports. Let's look at an example where we can see the resultant JavaScript code that comes from some examples here. So what we have here, y = foo, so I'm just gonna create a variable here so we can see where it pops up, and down here, this piece of code, you can see that this is the code that represents this index .ts file.

[00:09:56]
And what you can see here, if I comment this line out, just note this section here at the bottom, then I'm gonna uncomment it. So, you see how it's saying file name index.ts and it's added this require here. This is TypeScript saying you're actually using this as a value and when we compile this code and you need to run it somewhere, we really need to import this thing.

[00:10:23]
Before I did this, before I added line 16 here, I can make it a little bigger. Before I added line 16, there was no need to import this. It's something that could sort of just melt away as part of the type checking process, but we've crossed a threshold and we can go back over it and watch it disappear.

[00:10:42]
Look at that. See, there's the comment and then it's back, right? As soon as we need to use it as a value, TypeScript knows about this and it really becomes an import. It's part of the compiled result. That's what TypeScript does. If you use Babel, if you use Webpack, other bundlers like that, at first glance, they might say they work with TypeScript, which is true, but they don't understand TypeScript.

[00:11:12]
They effectively just know which parts of syntax they can safely ignore or delete as part of producing result in JavaScript. And they operate in a mode maybe we can even do it here if I zoom out. There's a mode the compiler can run in called isolated modules. It probably won't work in the playground, so we won't do this.

[00:11:42]
But really, each module is being transformed from TypeScript to JavaScript one at a time. There's no holistic analysis across files. And so what that means is you're always gonna see that import. There's there's no knowing whether this is a type or a value or what kinds of things you're using it as, when Babel goes through and it looks at this it just knows that it can eat that piece of syntax and not include it in the resultant JavaScript file, it's not doing type checking.

[00:12:18]
Now if you're using TypeScript with Babel or TypeScript with Webpack, something like that, TSC, that TypeScript CLI command, or the programmatic equivalent, it will type check your code, right? So you'll see errors and things. You'll see the same kind of developer feedback as if you were using TypeScript to actually admit your JavaScript.

[00:12:39]
So you won't suffer there, but where you will suffer is code splitting or dead code elimination. You'll have all of these imports in a variety of places where they could safely be discarded. But the fact that all Babel or webpack knows how to do is lose pieces of syntax.

[00:12:59]
It doesn't have the holistic understanding of your program that's necessary to know that an import is safe to disregard, so it includes all of them. Now, that is the main motivating statement for why you would want to use a type-only import. It does two things. One is, it'll bust you if you attempt to use a type-only import as if it's a conventional import.

[00:13:23]
And then the second thing is, webpack and Babel and other similar tools, they know that this is a type-only import and they know that it is safe to drop this. Because the type checking is enforcing that it will be safe to drop. That is the importance of, using type-only imports.

[00:13:44]
It allows you to avoid entangling with other modules when it comes to code splitting or tree shaking. Sometimes it's called tree shaking. It's dead code elimination is what's happening behind the scenes. But it allows you to really shake this modules loose, cuz we don't really need Strawberry at runtime.

[00:14:02]
We just need it for type checking. That makes sense? And if you're writing a library by the way, it's gonna get used in Webpack or Babel. It's a good idea to use this, especially if other people might consume your code and use it in different ways. So I tend to use these whenever I just need the types.

[00:14:27]
Yes?
>> Is there a TS config that will just warn you if you forget to be explicit about it?
>> There's probably a lint roll, but not not a TS config option that I'm aware of,
>> Okay, cuz it seems like something easy to overlook or forget or maybe you were using it now you removed it and you-.

[00:14:47]
>> It is and even if you try to be vigilant and you say, I'm gonna start out with the type-only import and when I crossed that threshold I will make it a proper conventional import. But then you could delete that, right? And then there's nothing there to help you stay in sync.

[00:15:04]
If that lint roll doesn't exist yet, hello, open source community. There's a very good lint roll opportunity here. If it doesn't exist yet, maybe go build it.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now