Before ES2015, JavaScript did not have a native module system. You had to use some third-party tools such as RequireJS or SystemJS to add support for modules. If you are using Webpack to bundle your project, then you do not need to worry about native module support at runtime. Webpack injects helpers to load module synchronously and asynchronously.
A module, in a nutshell, is a JavaScript file that operates in a sandbox environment and exposes some values to the public. It’s a normal JavaScript file with import
statements to import modules and export
statements to make a value (such as variable, function, class, etc.) available for import.
💡 To know more about modules and how they differentiate from normal script files, read the “Script vs Module” section of the Type System lesson.
TypeScript supports the EcmaScript module syntax which means you will be using modules in TypeScript using import
and export
keyword which is very convenient. In TypeScript, you can only import
file ending with .ts
and .d.ts
extensions (and .js
file as well but we will go into that by the end of this article). When you import these files using import
statement, they become modules.
💡 A file with
.ts
extension is a standard TypeScript program file. A file with.d.ts
is also a TypeScript file but this file contains type declarations. This topic is covered in the Declaration Files (coming soon) tutorial.
There are two ways to import a file in a program. First is by using a path of the file on the disk and second is by using a module name.
// example.ts
import "../program";
import "package";
From the above example, the first import
statement contains a relative path of program.ts
or program.d.ts
. Hence the example.ts
will first try to import program.ts
file relative to its own position in the file system. If program.ts
is not found, then program.d.ts
is imported. In the above case, program.ts
will be imported from the parent directory of example.ts
.
💡 You can also provide an absolute file path of a module in the
import
statements.
However, the second import
statement doesn’t contain a relative path. This is where things get a little complicated. When a non-relative (or non-absolute) path is used in the import
statement, the TypeScript compiler starts looking for a suitable module file in the file system to import.
💡 When I say a non-relative path, I mean when the
import
statement is specified without a relative or an absolute file path such asimport 'package';
in the above example.
Whether a relative path or non-relative path, when the TypeScript compiler encounters a import
statement, it starts looking for a module file to import in the file system. The lookup process is called module resolution. Depending on the path of a module, relative or non-relative, the TypeScript compiler takes a different approach to look for the module file. This approach is called a module resolution strategy. Let’s see how they work.
Module Resolution Strategies
When the TypeScript compiler sees a non-relative path in the import
statement, it has to find a file to import in the program since the import path doesn’t provide necessary information where the file is located on the disk.
The TypeScript compiler uses one of the two strategies available to it to find this file. These strategies are Classic
and Node
. Node
is the default strategy used by the TypeScript compiler and most people prefer this since most of the third-party modules are Node modules.
The Classic
strategy is present in TypeScript only for the backward compatibility with older versions. This strategy only kicks in if the TypeScript compiler finds a non-relative import.
The No``de
strategy comes from the standard module resolution strategy of the Node.js. This applies to a relative as well as non-relative imports.
💡 You can use the
--moduleResolution
flag with thetsc
command or use themoduleResolution
compiler-option to provide one of these strategies to the TypeScript compiler. These options are discussed in the Compilation article.
Classic Module Resolution
In the Classic
module resolution strategy, the TypeScript compiler will look for a file with the name same as provided in the import
statement and ending with .ts
or .d.ts
extension. It will start looking for the file from the current directory of the file with the import
statement and move up until the last directory in the file system is reached.
// /root/dir/sub/example.ts
import "package";
In the above example, let’s consider /root
is the parent directory of the file system. Since the import
statement contains a non-relative path, the TypeScript compiler will first look for the file in the /root/dir/sub
directory.
If neither package.ts
nor package.d.ts
file is found, it will move up a directory and search the same files in the /root/dir
directory. If it doesn’t exist there as well, it will finally search inside the /root
directory. If the file doesn’t exist after scrutinizing all the directories, it will throw an error.
// non-relative import - Classic strategy
Import statement: import 'package';
Import Location: /root/dir/sub/example.ts
1. /root/dir/sub/package.ts
2. /root/dir/sub/package.d.ts
3. /root/dir/package.ts
4. /root/dir/package.d.ts
5. /root/package.ts
6. /root/package.d.ts
7. Error: Cannot find module
Node Module Resolution
You might wonder why TypeScript needs to support Node’s module resolution strategy? This is because you might be writing a TypeScript program that is going to eventually run on Node. If that’s the case then you probably going to have a node_module
directory with some public packages.
💡 If you want to know more about Node.js and how it works, you can follow this article. I have given a brief introduction on Node.js and here I have discussed about
node_modules
directory.
We can provide an appropriate value for the module
compiler-option (discussed in the Compilation lesson (coming soon)) to the TypeScript compiler and it will convert all import
statements into require()
statement in the output JavaScript code. Since the default module resolution strategy during the compilation is Node
, the output code when running inside Node will produce the exact same behavior when it comes to module resolution.
💡 We won’t be discussing about TypeScript compilation mechanism in this tutorial. This has been discussed in a separate article (coming soon).
The Node
module resolution strategy is a little complicated to understand if you are not a Node.js developer. Similar to the Classic
module resolution strategy, the TypeScript compiler looks for the file by traversing directories in the outward direction, however, there are multiple checks involved.
// /root/dir/sub/example.ts
import "../program";
import "package";
From the above example, the first import
statement contains a relative import path. In this case, the TypeScript compiler will look for the program.ts
or program.d.ts
file in the parent directory of example.ts
.
If it doesn’t exist, then it will check if program
is a directory inside the parent directory. If it is and it contains a package.json
file, then it will import the file specified by the path in the types
or the typings
property of this JSON file.
If package.json
file doesn’t exist in the program
directory or it is missing the types
property, then it will check for the index.ts
or index.d.ts
file in the program
directory. If none of these works, it will throw an error.
// relative import - Node strategy
Import statement: import '../program';
Import Location: /root/dir/sub/example.ts
1. ../program.ts
2. ../program.d.ts
3. ../program/package.json -> [[types]] -> file path
4. ../program/index.ts
5. ../program/index.d.ts
6. Error: Cannot find module
For the second import
statement, things get even more complicated. First, the TypeScript compiler will look for node_module
directory in the current directory of example.ts
and perform exactly the same operations of relative import discussed previously. If it can’t find a file, it will move up a directory and try to find the file using the same logic. This process is illustrated below.
// non-relative import - Node strategy
Import statement: import 'package';
Import Location: /root/dir/sub/example.ts
1. /root/dir/sub/node_modules/package.ts
2. /root/dir/sub/node_modules/package.d.ts
3. /root/dir/sub/node_modules/package/package.json -> [[types]] ->
4. /root/dir/sub/node_modules/package/index.ts
5. /root/dir/sub/node_modules/package/index.d.ts
6. /root/dir/node_modules/package.ts
7. /root/dir/node_modules/package.d.ts
8. /root/dir/node_modules/package/package.json -> [[types]] ->
9. /root/dir/node_modules/package/index.ts
10. /root/dir/node_modules/package/index.d.ts
11. /root/node_modules/package.ts
12. /root/node_modules/package.d.ts
13. /root/node_modules/package/package.json -> [[types]] ->
14. /root/node_modules/package/index.ts
15. /root/node_modules/package/index.d.ts
16. Error: Cannot find module
For the sake of simplicity, we are going to use only relative file imports of the .ts
files in this lesson so that we could see some working examples.
Module Standard
EcmaScript has standardized how we should be importing modules in JavaScript and this standard revolves around two keywords, import
and export
. As the module system in JavaScript is getting popular, there are some new changes coming to this standard every year.
In this tutorial, we are going to look at the semantics of the module system in TypeScript (which mostly resembles the EcmaScript standard) and how import
and export
keyword works.
Named Exports
The export
keyword makes a value (variable) defined in a file available for import in other module files. When a module is imported in another module file using import
keyword, the importing file can specify the values that it wants to access from the imported module.
// program.ts
import { A } from "path/to/values";
console.log(A); // "Apple"
In the above example, we are importing values.ts
in the program.ts
file and extracting the A
exported member. This means values.ts
must export the value A
in some form. There are multiple ways to expose A
.
// values.ts
var A = "Apple"; // can also use `let` or `const`
class B {}
function C() {}
enum D {}
export { A, B, C, D };
You can export any accessible TypeScript value with a valid identifier (name). Here in this example, we are exporting a variable A
(could also be a constant), class B
, and so on. Using export {}
syntax, you can export as many values as you want and anyone can import them using import { A, B }
syntax. You do not need to import all the exported values.
There is another way to export a value where it was declared.
// values.ts
export const A = { name: "Apple" };
export class B {}
export function C(){}
export enum D{}
In the above example, we have exported all the values where they were declared. You also do not need to initialize the value, you can do that later down the file. This is a much nicer way compared to the previous one.
In both cases, all your export members are accessible within the same file if you need to use them. Having export
keyword on a value doesn’t change its behavior within the module file.
Default Export
So far we’ve exported values that can only be imported by providing the name of export members such as import { A } from 'path/to/values'
where A
here is an export member of values.ts
. These are called named exports.
You are also allowed to export a single value that can be imported without specifying a name. This value should be identified with default
keyword along with the export
keyword.
// values.ts
var A = "Apple"; // can also use `let` or `const`
class A {}
function A() {}
enum A {}
export default A;
Only a single value is allowed to be exported as a default export, hence you don’t have export default {...}
expression to work with. Unlike named exports, a value can not be exported as default with a variable (or constant) declaration except for a function
and class
declaration.
// values.ts
export default var A = "Apple"; // ❌ invalid syntax
export default enum D{} // ❌ illegal: not a function or class
export default class B {} // ✅ legal
export default function C(){} // ✅ legal
However, you can export a value directly as a default export. This means any JavaScript expression can be exported as the default export value.
// values.ts
export default "Hello"; // string
export default {}; // object
export default () => undefined; // function
export default function () {} // function
export default class {} // class
Since default export lacks an export name, we can provide any name for it while importing. You can have named exports and a default export in the same module file. Similarly, you can import the default export and named export values in a single import
declaration as shown below.
import Apple, { B, C } from "path/to/values";
In the above example, the Apple
is the name we used to collect the default export from the values.ts
. You can drop the { B, C }
expression or Apple
name from the import
declaration if you are not going to use it.
💡 Like named exports, it is not mandatory to import the default export value when
import
declaration is used. But, theApple
must come before the named exports in theimport
declaration signature.
You can also use as default
syntax in the export {}
signature to export a value as default along with other named exports. This is called aliasing and it is explained in the next section.
// values.ts
var A = "Apple"; // can also use `let` or `const`
class B {}
function C() {}
enum D {}
export { A as default, B, C, D };
💡 The reason I don’t use and recommend
default
export has to do with developer experience. As we learned, you can provide any name for the default export memeber of a module in theimport
declaration. If this value is imported in multiple files, you can reference it with any name of your choice and having different names for the same export member of the same module is a bad DX.
Import And Export Alias
In the case of the default export, you can reference the value with any name of your choice in the import declaration. However, that’s not the case with named exports. However, you can use as keyword to reference a named export value with the name of your choice.
// program.ts
import A, { B as Ball } from "path/to/values";
console.log(B); // ❌ Error: Cannot find name 'B'.
console.log(Ball); // ✅ legal
In the above example, we are importing B
from the values.ts
but it has been renamed to Ball
. Hence in the program.ts
file, you would be using Apple
instead of B
. When you alias an export member, the original name doesn’t exist anymore in the global scope, hence B
won’t exist in the program.ts
anymore.
💡 You can’t alias the default export member since you can already provide any name of your choice but you can use
import { default as defaultValue, }
.
You are also allowed to alias a value while exporting using as
keyword.
// values.ts
var A = "Apple"; // can also use `let` or `const`
class B {}
function C() {}
enum D {}
export { A as default, B, C as Cat, D };
In the above example, we have exported variable A
as default export and function C
as Cat
. Hence anyone who imports these values from values.ts
file must use default import syntax for A
and Cat
for the C
value.
Import All Named Exports
You can import all the named exports from a file using * as
syntax.
// program.ts
import Apple, * as values from "path/to/values";
console.log(values.B);
console.log(values.C);
console.log(values.D);
Here, all the named exports from values.ts
will be saved under values
which will be an object where the keys
will be the name of the export members and values
will be the exported values of the export members.
Re-exports
An imported value can be re-exported in the normal fashion. When you import something, you have a reference to the import value and you can use the same value in the export
syntax. Nothing is wrong with that.
// lib.ts
import A, { B, C as Cat, D } from "path/to/values";
export { D as default, A, B, Cat as C, D };
In the above example, you have imported a few values from values.ts
inside lib.ts
and exported according to your preference. This is great if you want to use these imports in lib.ts
as well as export some of them if another file needs them when it imports from the lib.ts
.
However, we also have export ... from
statement to export values from a file without having to import them first. This is great when you want to keep a single point of entry for all the imports in your module.
// lib.ts
// re-exports
export { P as Paper, Q } from "path/to/values-1";
export { N, O as Orange } from "path/to/other/values-2";
// default export
export default "hello";
class B {}
function C() {}
// named exports
export { B, C as Cat };
In the above example, we are exporting some of the export members of values-1.ts
and values-2.ts
from the lib.ts
file (except the default export). We can also use aliasing in the export ... from
syntax.
The re-export syntax doesn’t affect the current file, hence you can have your regular exports in the file as well as shown above. Unlike export
keyword, export ... from
syntax doesn’t allow access to re-exported members. Hence you won’t be able to access P
or Orange
in the lib.ts
file.
// lib.ts
export { P as Paper, Q } from "path/to/values-1";
export { N, O as Orange } from "path/to/other/values-2";
console.log(P); // ❌ Error: Cannot find name 'P'.
console.log(Orange); // ❌ Error: Cannot find name 'Orange'.
You can export all the named exports from a file using export * from
syntax as we as you have the capability to rename them while exporting using export * as ... from
.
// lib.ts
export * from "path/to/values-1";
export * as values2 from "path/to/other/values-2";
From the above example, if values-1.ts
exports P
and Q
, then anyone can import P
and Q
from lib.ts
. However, all the named exports of values-2.ts
are rep-exported as values2
named export of lib.ts
, hence you need to import them using the following syntax.
// program.ts
import { P, Q, values2 } from "path/to/lib";
You can’t access default export using export ... from
syntax in a normal fashion. But you can export the default export using the default
keyword.
// lib.ts
export { default } from "path/to/values-1";
export { default as Apple } from "path/to/values-2";
In the above example, the default export of lib.ts
is the default export value of values-2.ts
. The default export value of values-2
will be exported from lib.ts
as the Apple
named export.
Import for side-effect
Can you imagine importing a file without specifying the exported members?
import "path/to/action";
In the above example, we are importing action.ts
but we have specified any members exported by the action.ts
. This is a valid syntax but it is used to produce a very different result.
When you import a file using import
keyword, it’s first executed and then all the export members are available for import. Hence in the below example, a file importing PI
would get the 3.14
as a floating-point
number.
export const PI = parseFloat("3.14"); // 3.14
Following the same logic, if you want to create a side-effect by importing a file, then you can totally do that. So for example, if there is a file that initializes some global variables, you can import that without specifying the exported members of that file (it also could export none in the first place).
TypeScript compiler can create a JavaScript bundle file (.js
) by compiling two or more .ts
files together. This is normally done by providing an entry TypeScript file and then going through its dependency tree.
A dependency tree is constructed by looking at all import
declaration (dependencies) of the entry file and following the dependencies of these imported files. If you want to include a file whose code might create some side-effects at runtime, then you should import that file without specifying the exported members in the import
syntax. This can be done in the entry file or inside any file that is inside the dependency tree.
Share Type Declarations between modules
A type declaration is a declaration of a type such as an interface
type or function
type. You can export and import TypeScript types like regular values between modules using the same import
and export
statements.
Since TypeScript is an erased type system, all the types defined within a TypeScript file are removed from the compiled JavaScript code. Therefore, TypeScript also removes the import
and export
statements that only transport TypeScript types between modules.
// b.ts
export interface PersonInterface {
fname: string;
lname: string;
fullName(): string;
export class Person implements PersonInterface {
constructor( public fname: string, public lname: string ){}
fullName(): string {
return `${ this.fname } ${ this.lname }`;
}
}
In the above b.ts
file, we are exporting the interface PersonInterface
which is a TypeScript type and the Person
class. These members are imported by a.ts
as shown below. Remember PersonInterface
is a type, so it must be used as a type and not as a value.
// a.ts
import { Person, PersonInterface } from "./b";
const p: PersonInterface = new Person("Ross", "Geller");
In the above a.ts
file, we have imported both Person
class and PersonInterface
interface from b.ts
. The program should be fairly easy to understand. Now let’s compile this program by setting --module
to ES6
and --target
to ES6
just to see how it will look in the compiled JavaScript.
$ tsc --module ES6 --target ES6 *.ts
💡 We have a separate lesson (coming soon) on the TypeScript compilation process where I have discussed the
tsc
command and command-line flags.
Using the above command, the TypeScript compiler generates a.js
and b.js
files. TypeScript also erased types from the compiled output.
// a.js
import { Person } from "./b";
const p = new Person("Ross", "Geller");
// b.js
export class Person {
constructor(fname, lname) {
this.fname = fname;
this.lname = lname;
}
fullName() {
return `${this.fname} ${this.lname}`;
}
}
As you can see from the code above, the b.js
does not contain export interface PersonInterface
statement anymore. Similarly, the a.js
does not an import of the PersonInterface
interface type anymore.
This proves that types in TypeScript can be imported and exported like regular JavaScript values but their import
or export
statements do not appear in the final compiled JavaScript.
💡 In Type Declaration Files (coming soon) lesson, we have discussed the structure of a TypeScript file that only contains type declaration as well as different mechanisms to import these declarations besides using
import
statements.
Dynamic Imports
Now that we know how the module system works in TypeScript, let’s understand some core concepts about module loading in JavaScript.
Module Caching
A module is initialized only once when the first import
statement is encountered at runtime. This means even though your program contains multiple import statements of the same module, the JavaScript engine returns the already initiated instance of the module.
This is ideal when you have a module import that produces a side-effect. If you have imported such a module at multiple places, it will produce the side effect only once.
Import Position
You can’t import a module in a scope other than the global scope such as in a function. That means all the import statements should appear on the file scope, preferably, on the top of the file. Putting an import
statement inside a function or a conditional block would result in a compilation error.
If the import
statements are allowed inside a function body, the TypeScript compiler would need to perform control flow analysis to determine when the module will be initialized. If the invocation of an import
statement depends on the runtime environment, the TypeScript would need to simply disable the type-checking for it and blindly allow it. This would be a huge problem.
Module File Path
In the first section of this article, we learned about module resolution strategies. We learned that there are two ways to import a module, by providing a relative file path or a non-relative path.
Whatever the module path is, it must be a string
and a compile-time constant. This means that the import path shouldn’t be produced by a JavaScript expression like a template string, a function call, or string concatenation expression.
Dynamic Loading
It sounds like the TypeScript compiler needs to know about the module at compile-time so that it can locate it and process it. But there is a mechanism to import a module dynamically using import()
expression.
The import()
expression shares some similarity with import
statement but import()
takes a module path (relative or non-relative) as an argument and returns a promise. This module path is evaluated at runtime, so it can also be an expression that returns the string
.
The promise returned by the import()
expression is resolved into an object that contains named exported members as well as the default
export value with the key default
. The promise may reject if the import path is invalid.
The great thing is, the import()
expression can be placed anywhere in the code. This means a module can be loaded dynamically based on our needs or some environmental conditions at runtime. Due to this behavior, they are called dynamic modules.
function run() {
import("path/to/values")
.then((mod) => {
console.log(mod.default);
console.log(mod.A);
console.log(mod.B);
})
.catch((error) => {});
}
Since the import
function returns a Promise
, we can use await
keyword to wait until the module is resolved.
async function run() {
const mod = await import("path/to/values");
console.log(mod.default);
console.log(mod.A);
console.log(mod.B);
}
Since the module path provided to the import()
expression can be dynamic unlike a string
constant, TypeScript can’t always guess where the module file is located in the file system. For example, if the module path is resolved using a function call, such as import(getPath())
, then TypeScript won’t be able to analyze the module statically.
When the import path is a JavaScript expression, TypeScript won’t provide any type based Intellisense since it doesn’t know which module is being imported at runtime. All import values from such a module will have the type of any
.
Since TypeScript can’t locate such module files at compile-time, they do not become part of the compilation. Therefore, it becomes your responsibility to manually include these module files using files
and include
compiler-options of the tsconfig.json
(see the compilation lesson (coming soon)).
A sample TypeScript project
Now that we have covered pretty much everything there is to imports and exports in TypeScript, let’s implement this knowledge by building a quick and simple project. It’s gonna be really simple, I promise.
We are going to build a library that lets you create a School
object that contains some people. These people have some roles, such as a person can be a student or a teacher. These objects can be created by instantiating their respective classes with appropriate arguments.
/projects/school
├── lib
| ├── api.ts
| ├── models
| | ├── types.ts
| | ├── Institution.ts
| | └── Role.ts
| └── utils
| └── getAge.ts
├── node_modules
| └── moment
├── package.json
├── src
| └── program.ts
└── tsconfig.json
The file structure above is how this project will look like. Let’s walk through this file tree and understand job of each file in the project.
- The
lib
directory stands for the library. It contains all sharable units that a program in the project can import and use. - The
models
directory contains all the classes. TheInstitution.ts
file exports theInstitution
class that can act as aSchool
orCollege
which contains people with roles like students and teachers. - The
Role.ts
file contains classes that let you create person objects of each role. It also contains a base class that these person classes of each role extend to. It also exports an enum that contains predefined role types. - The
util
directory contains some utility functions such as getting the age of a person from his/her DOB which should be an instance ofDate
. - The
src
directory contains programs that we are going to execute on Node usingts-node
. Theprogram.ts
imports necessary classes and values fromlib/api.ts
. Theapi.ts
file provides all the necessary elements from the file in thelib/
using re-exports
The getAge.ts
file exports a function as the default
export. This function takes a Date
object and returns a number
which would be the age of the person. If you notice, this file imports from the moment
path.
Since this path is non-relative, the TypeScript compile will follow the Node
module resolution strategy by default. I have installed moment
Node module using npm install moment
command which would create a node_modules
folder in the current directory and copy the module files.
This mo``ment
package include a package.json
that has the typings
property which points to the TypeScript declaration file in the package. This is how the TypeScript compiler knows about the values exported by this package.
Since moment().diff()
function returns a number
and the TypeScript compiler can confirm that from the type definition file provided by this package, we can provide number
as the return type of exported function.
The types.ts
file contains type declarations used across the project. At the moment, this file exports RoleInterface
that will be implemented by the Role
class as shown below.
The Role.ts
file contains Roles
enum that specifies the role of each person contained in the school. The Student
and Teacher
class extends Role
class that provides common properties and methods for these two.
In the end, we are exporting Roles
enum as the default export of this file, as well as Role
superclass, and Student
and Teacher
subclasses.
The Institution.ts
file imports the classes from Role.ts
and renames Role
import as Person
since it makes more sense as Role
class describes the basic properties of a person. Roles
enum value is the default export of Role.ts
.
This file exports the Institution
class that contains a list of student objects of type Student
and a list of teacher objects of type Teacher
. It also lets you add an object of type Person
to these lists using addPerson
method.
Since both Student
and Teacher
inherits Person
class, a valid Student
or Teacher
object can also have the type of Person
. This is polymorphism in a nutshell. The instanceof
type guard helps the TypeScript compiler narrow the Student
or Teacher
type from the Person
type.
The findPerson
method finds the person in the Institution
using id
of the person and the kind
value which is the Role
of the person. A caller of this function must specify the kind
value using Roles
enum.
The api.ts
file provides an interface to interact with Institution
but it renames the Institution
class to School
class using import alias. It also re-exports Roles
enum (as default export) as well as Student
and Teacher
class from Roles.ts
. This is a standard library pattern when the user only has to worry about a single entry point of the library which is api.ts
in our cases.
The program.ts
file contains a program that we are going to run on Node to execute some meaningful task. This file imports all the values exported by the api.ts
file. This program is pretty straightforward, so I won’t be explaining every single line.
The findPerson
method of the School
class returns an object of type Person
that has the getFullName
method, hence calling s.getFullName()
would not be an issue, same goes for the t
.
However, despite these values being an instance of Teacher
or Student
, since the return type of findPerson
is Person
, the TypeScript compiler won’t let you call canTeach
or getAge
methods since they don’t exist on the type of Person
, hence we have used type assertion.
We can run this program on Node by the help of ts-node
using a simple command such as ts-node src/program.ts
assuming that we are in the project directory. However, this program won’t run due to some TypeScript compilation errors. What could be the problem?
In our library code, we have used .find()
prototype method on an Array
and .includes()
prototype method on a string
. These do not have support in the ES5 version of JavaScript. Unless we specify a target
version, the TypeScript compiler assumes that we are compiling for ES5
.
This can be solved by providing a tsconfig.json
file. We are going to learn about this file in the compilation and config file lesson but the following is the configuration of the file we are using.
Since includes
and find
method was introduced in recent versions of the ECMAScript, we need to provide ES2017
(or higher) as the target
of compilation. Using this TypeScript compiler won’t throw any error since ES2017
has support for these methods.
Publishing the package
First of all, if you want to publish this project as a library then src/program.ts
file should not be in the picture since it is a program file and not something that should be imported. It doesn’t export anything anyway.
You typically publish the package as an NPM module that other people can install it using npm install <module-name>
command. We have discussed the structure of NPM modules in this article.
Let’s say we want to publish this project as a node module (a package for Node.js), then the output code should contain all the import
statements replaced with the corresponding require()
calls since Node.js uses the CommonJS
module system. This can be done using module
compiler-option.
💡 I think you need to read the Compilation lesson (coming soon) to make sense of all of the concepts discussed in this section.
We are going to use this tsconfig.json
file which only includes lib/api.ts
file in the compilation since other files are automatically considered by the TypeScript compiler due to their import statements.
The target
is set to ES2018
to include all the JavaScript features up until ES2018
standard. The module
is set to CommonJS
since we want to use the compiled JavaScript code inside Node.js environment. The declaration
option is also set to true
so that the TypeScript compiler could generate .d.ts
declaration files in the output directory specified by outDir
.
Once we run tsc
command which uses the tsconfig.json
to compile the project, we get the following output file structure.
/projects/school
├── dist/
| ├── api.d.ts
| ├── api.js
| ├── models/
| | ├── Institution.d.ts
| | ├── Institution.js
| | ├── Role.d.ts
| | ├── Role.js
| | ├── types.d.ts
| | └── types.js
| └── utils/
| ├── getAge.d.ts
| └── getAge.js
├── lib/
| ├── api.ts
| ├── models/
| | ├── Institution.ts
| | ├── Role.ts
| | └── types.ts
| └── utils
| └── getAge.ts
├── package.json
└── tsconfig.json
Normally, we only publish the dist
directory and keep the lib
directory private. You can use the .npmignore
file to do that which avoids including files being pushed in the package, just like how .gitignore
works.
The package.json
declares the name
and version
of the package, dependencies of this package as well as other critical information. The most important fields of this file are main
and typings
.
// package.json
{
"name": "school",
"version": "1.0.0",
"main": "./dist/api.js",
"typings": "./dist/api.d.ts",
"dependencies": {
"@types/moment": "^2.13.0",
"moment": "^2.26.0"
}
}
The main
field points to the that will be imported when the runtime encounters require('school')
import statement while the typings
(or the types
) field points to a declaration file that will be used by the TypeScript compiler and IDE to fetch type declarations of the package. We have discussed about this file in the Declaration Files lesson (coming soon).
The package is ready to be published. Once the package is published using npm publish
command, one can install it using npm install school
command which would put the above files in the node_modules
directory.
Now when the user imports the package using require('school')
statement, s/he should get exported values from the dist/api.js
file. Similarly, the IDE would refer to the dist/api.d.ts
file to provide Intellisense for the API structure of this package.