You can use the
sample.js module in TypeScript code without any problems. But guess what? The analyzer would not be able to run autocomplete and infer types properly. If we want to rely on help from smart tools, we need to manually describe the API provided by our JS module. Usually, it is pretty straightforward to do:
export const pageSizes = [25, 50, 100] from the
There are many examples of how to write type definitions. Most of the time you will meet simple cases and thus would be able to find something similar in the repository called DefinitelyTyped, where developers store definitions for NPM packages. You can also learn more about the type definitions feature in the official documentation. It is not a part of this article.
In our company, we develop an internal UI components library. We use it in our products from the beginning, and the current production version is 12. You could only imagine how much effort it would take to rewrite such a big thing. In the meantime, we write new features using the TypeScript language. The problem is, every time one team goes to implement a new code, they write a small copy of the UI library definitions. Well, this does not sound like a good process, and we decided to have a separate package with complete type definitions for our UI components. Key points here are:
- We would be able to import this package during the new repository initialization. This will allow controlling the version and simplify the refactoring during the version update.
- We would stop copy-pasting the same code again and again.
- Type definitions is a great documentation source. I bet developers would prefer to select the method from IntelliSense suggestions rather than go to the web page with all API descriptions and copy the method name.
Now you may ask me, what is wrong with our library? The thing is that we inject some global variable to interact with the exposed API. In addition, we want to import some constant pre-defined values (icons, table cell types, tag colors, etc.) that can be used by the UI components. They usually come in form of constant identifiers that help to style components.
For example, we can style a button with one of the types:
We came to an idea to store all library-specific values in one place. So this project became not just type definitions for the UI library, but a real package! It should represent the exact library state at some specific version. And this is interesting – how can we implement this? Let’s state what we want to achieve as the result:
- We want the global variable
uito be accessible without having to import anything.
- We want our UI components definitions to be available without having to import anything as well.
- We want to use predefined constants and objects for UI components by importing them from our types package. There should not be any conflict to assign some type from the library in this case.
Sounds like a small deal, right? Let’s write some
.d.ts file with type definitions and… Oh, wait, you can’t put real code (constants, enumerable lists, and other stuff) in the
.d.ts file! Sounds reasonable. Let’s create a regular
.ts file and put all these enums there. Then we… well, how can we apply globals in the
.ts file?! Meh…
We did not find an example of how to do that, really. StackOverflow is flooded with the
.d.ts vs .ts concept war. We had nothing but digging into TypeScript documentation and finally introduced the code that meets our requirements.
First things first. We write interfaces and enums as usual. I am going to provide code examples in a simplified matter, so we would focus on the approach, not the particular code features. Imagine we have a notification dialog, so we write something like this:
ButtonType values are from enum we saw already:
Then let’s take a look at the simple case. We don’t import anything, as the UI components expose the global variable, and we want to call a notification:
What do we need to do to make it available? We are going to enrich the global namespace with the
UiLib here describes everything our UI library exposes into the global scope. In our example, we have a list of methods that show different kinds of notifications:
This is almost it. Lastly, we adjust the package configuration. We tell TypeScript to emit type declarations by adjusting the
We now control how TypeScript emits the output. We also specify a path to our types in
Alright, then we install the package in our project. Finally, we specify the package path in the project’s
tsconfig.json (since we don’t use the default
@types folder) to see that it works!
Now let’s go deeper. What if we want to create a notification with some specific button? We want to be able to write something similar to this example:
Note here and below UiCore is a namespace that contains all the enums, configs, interfaces our UI library operates with. I think it is a good idea to collect everything under some namespace, so you would not think of names for each interface. For instance, we have a
Notification interface. It sounds quite abstract, and it takes a while to understand the exact object behind the naming. In the meantime
UiCore.Notification clearly describes where it comes from. Having a namespace is just an optional but convenient way to handle such things.
Right now we can’t import
UiCore from the library as we don’t export anything. Let’s improve our code and form the namespace:
We basically export all data we have under the namespace with
export import alias syntax. And, since the main package module is
index.ts in the root, we write a global export to expose the namespace to the public:
Two simple steps to achieve our goal! Now we can import some enum and enjoy writing the code. OR. Or we can think of some other use cases. In the example above, we used the
ButtonType.Danger value to create a notification with some pre-defined button. What if we want to use
ButtonType as a parameter type?
We are not going to use so