In TypeScript, what does it mean for an import or export to be "top-level"?
Top level import
is a static import that is located at the very top of the file. However it's called "top-level" not because it is located at the top of the file, but because there are dynamic imports that are not top level:
import foo from 'foo' // top level import, static one
import('foo').then(/* ... */) // not top level import, dynamic one
// no static top-level import can live here (after any code that is not a top-level import declaration)
function bar() {
import('foo').then(/* ... */) // not top level import, dynamic one
// no static top-level import can live here
// no export can live here too
}
// no static top-level import can live here
export const baz = 123 // exports are always top level, and static
// You still can add not top level imports here, in the very end
import('foo').then(/* ... */)
Now, why this matters in Typescript?
Well if you put two files without a top-level import/export, that has two identifiers that are the same, you will get an error:
// a.ts
let foo = 1 // Error: duplicate identifier
// b.ts
let foo = 1 // Error: duplicate identifier
This happens because there are no top-level export/import declarations and TS considers these files to be scripts
(in contrast to modules
). And what happens if you load in a browser two scripts with same identifiers? Right, a "duplicate identifier" error will rise. Because both variables live in the global namespace.
Therefore, to avoid that you can do this:
// a.ts
let foo = 1 // Ok
// b.ts
let foo = 1 // Ok
export {} // This is the magic. b.ts is now a module and hence, is not polluting the global namespace.
The top-level in typescript is the outer most scope.
What is a "scope"?
You create a scope every time you open a set of {
braces¹.
Scope limits the visibility of variables and functions to the scope they are defined in and child scopes.
For example:
import { something } from "<module>"; <-- Global / Top-level scope
function divide(x, y) { <-- Start function scope
if(y == 0) { <-- Start of the if's scope
/*
* Here is a child scope of the function
* This means x and y are available here.
*/
var error = new Error("Cannot divide by 0); <-- "error" is only available here.
throw error;
} <-- End of the if's scope
/*
* The "error" variable is not available here
* since the scope it was defined in, was already closed.
*/
return x / y;
} <-- Ends the functions scope
var z = 0; <-- Global scope
This means:
import { x } from "<module>"
/* rest of code */
works but for example:
if (true) {
import { x } from "<module>";
}
does not work, since the import is wrapped in the scope of the if statement and therefore is not at the top-level scope anymore.
But this does not mean the "top-level" is at the top of the file. It just means the outer most scope.
function add(a, b) {
return a + b;
}
import { x } from "<module>";
This still works, since the scope of the function ends with the closing }
brace¹. Meaning everything after it is at the top-level again.
Everything said about import
's also applies to export
's
1: Sometimes you can omit the {|}
braces to create a new scope. You still create a new scope, but do so implicitly.
For example, consider the two snippets below - They are the same. Languages create scope lexically - scope is not defined by tokens
if(true)
return true;
else
return false;
is the same as
if(true) {
return true;
} else {
return false;
}