The Variety of Type Definitions in TypeScript

TypeScript stands apart from other languages largely because of the many different kinds of type definitions it supports.

If you've used TypeScript with Angular—or even on its own—you've likely encountered questions or error messages like:

  • Does type safety in TypeScript force me to write more verbose code?
  • What different categories of TypeScript type definitions exist?
  • How can I work with libraries that lack type definitions?
  • How do type definitions relate to Npm?
  • When is it appropriate to install third-party types?
  • How do packages include their own custom types?
  • What is @types, when should it be used, and for what reason?
  • What became of the typings command-line tool and DefinitelyTyped?
  • What are compiler opt-in types, and when do they make sense?
  • Why does a 'duplicate type definition' error occur sometimes, and what's the remedy?
  • Why do Promise type definitions seem to malfunction on occasion?
  • What are the recommended approaches for using TypeScript type definitions effectively?

All of these topics will be addressed in this guide. I suggest you follow along by creating a new empty project folder, since the explanations will be driven by practical examples.

The journey may feel a bit rocky at times, but the final conclusions section will tie everything together and offer practical advice for making the most of available type definitions.

The payoff of using them properly is significant, yet it does require some familiarity with where to locate the right definitions and which ones to choose in different situations.

Let's dive in and begin.

What Are the Possible Scenarios for TypeScript Type Definitions?

When integrating a JavaScript library into a TypeScript project, you'll typically encounter four distinct situations regarding type definitions:

  • No type definitions exist for the library
  • The type definitions are included directly with the TypeScript compiler
  • The library doesn't come with types, but they're available for separate installation
  • The library packages its own type definitions

What distinguishes these cases? Let's start with the scenario where no type definitions are available, since that's quite common and likely will remain so for the foreseeable future.

There's no guarantee that future JavaScript modules will inherently include their own types, or that someone will take the time to write and maintain them.

Well-known, widely-used modules will probably get good type support, but what about the smaller, less popular ones?

Working with Libraries That Have No Type Definitions

As a first example, let's initialize a node project in a blank directory and install a module called uuid, which creates unique identifiers.

Note that we've used npm init to create a basic package.json and have set up a local TypeScript installation. If you open the project in an IDE like Webstorm, it will automatically pick up the TypeScript version found inside node_modules.

There's no need for a global setup, and it's often better to avoid it to prevent version mismatches between your project, terminal, and IDE.

With uuid installed, how do we go about using it?

The Shift Between TypeScript Versions Before and After 2.1

First, we should verify which TypeScript version is active. Suppose we're using a release from before 2.1, like 2.0.10. What occurs if we attempt to import the uuid library? Let's test that.

Let's examine the import statement more carefully:

  • Using the ES6 import syntax, we're pulling a module named uuid
  • The syntax with * indicates we expect a default export
  • We're catching that single export and assigning it to a constant called uuid, but what will its type be?

The uuid variable has silently been given the type any, and we're applying it as a function. Let's run this to observe what happens.

Running TypeScript Files Easily

Since this is a TypeScript file, we can't just execute it with Node. Does that mean we need a complicated build pipeline? Not necessarily. We could set up a simple npm script that invokes the tsc compiler and then runs the resulting JavaScript.

To keep our directories free of generated files, we can alternatively use a tool called ts-node.

After installing ts-node, we can add an npm script to execute this test file, named test.ts.

Now we can run the test program with a single npm command.

The output depends on the TypeScript version in use. Here's why.

Outcomes with TypeScript Versions Prior to 2.1

If you're on TypeScript 2.0.10, for instance, you'll see this error:

test.ts(3,23): error TS2307: Cannot find module 'uuid'.

Why is the compiler unable to locate the module? We know the CommonJs uuid module is in node_modules, so why does the compiler claim it can't be found?

Starting with version 2.0, the compiler is supposed to look inside node_modules by default, correct?

The issue is that while a uuid module is present, it doesn't ship with its own type definitions. So how can we use it with this TypeScript version?

Node Type Definitions Are Missing

One option is to go back to the CommonJs require syntax. Let's give that a shot.

This time we'll encounter the following console error:

test.ts(3,14): error TS2304: Cannot find name 'require'.

This error occurs because the TypeScript compiler has no knowledge of the require function. How do we fix that?

TypeScript 2.1 and Standard JavaScript Modules

I'll show you how to use Node's require in TypeScript later. For now, the easiest fix is to upgrade to TypeScript 2.1 or newer and switch back to the import statement.

With that change, everything compiles as anticipated. In TypeScript 2.1, any CommonJs module in node_modules without its own type definitions can still be imported and used.

But how can we invoke it as a function? What type does uuid have?

The imported module is simply given the Any Type implicitly.

The Behavior of the Any Type

The Any type effectively lets you circumvent TypeScript's type safety:

  • You can call a variable of type Any as a function with parentheses, just like we did with uuid
  • A variable typed as Any is assumed to have any property, much like a standard JavaScript object
  • You can assign a variable of type Any to essentially anything else without encountering an error

Implications of the Any Type

While our program compiles, using Any in this manner essentially means we're writing plain JavaScript for that library. This means no useful auto-completion or dependable refactoring support.

However, the benefit is that any module on npm can be easily integrated into a TypeScript application.

That's a solid foundation. If everything else fails, our JavaScript will just work. But how can we gain type safety for the most common npm libraries?

The Relationship Between Type Definitions and Npm

Let's move on to modules that come with their own types. An increasing number of npm packages now include built-in type definitions.

These types are packaged within the node module and don't need a separate installation.

For example, let's install the Axios HTTP library. If you're unfamiliar with it, Axios is an excellent tool for making Ajax calls in the browser and HTTP requests on the server, both using the same Promise-based interface.

This command installs Axios, enabling us to call a REST API using regular JavaScript. The good news is that we can also do it with full type safety.

Axios includes its own type definitions, as we can verify.

Looking inside the module, we can see that Axios bundles its own typings, so separate installation is unnecessary. Let's use it in our test program.

We imported Axios's default export and named it axios. Its type is automatically AxiosStatic, as declared in Axios's type definition file.

Is Manual Type Annotation Required for Type Safety?

The axios import is not of type Any, meaning we automatically get features like auto-completion, refactoring, and finding usages.

Also, take a look at the AxiosPromise type annotation—it's actually not needed. Removing it won't matter because the constant's type would still be inferred as AxiosPromise, and auto-completion for that variable would keep working.

What about the configuration object following the url? It's automatically typed as AxiosRequestConfig, providing auto-completion for request parameters as well.

Type Safety Doesn't Have to Mean Extra Boilerplate

Why isn't there a compilation error when we pass an empty object at this stage?

The AxiosRequestConfig interface only defines optional properties. Our IDE can show us this definition.

Every property is indeed marked as optional with a question mark. Thus, this is a clear example of how using a library with its own type definitions doesn't force you into verbose code or frequent error squiggles.

The Primary Advantage of TypeScript

Leveraging TypeScript lets you keep the convenience of JavaScript while gaining better tooling. When the libraries you depend on have built-in types, features like auto-completion, refactoring, and find usages become available all over your codebase, requiring just a few deliberate type annotations.

The main exception is function parameters, where the compiler can't infer the type without you specifying it. Adding type annotations there is also excellent for documentation.

Getting the Most Out of TypeScript Type Definitions

To fully harness TypeScript's type inference and have it automatically determine types for as many variables as possible, head to tsconfig.json and activate the noImplicitAny option:

When this is turned on, the compiler won't implicitly fall back to the any type if it can't infer a variable's type. It's arguably one of the most critical compiler settings.

The name useTypeInferenceAsMuchAsPossible might have been a better fit than noImplicitAny.

Now, let's look at Axios's type definitions and see how they aid in writing type-safe code.

Using the Promise API in Your Code

Suppose we want to write a method that performs an HTTP call and returns a typed Promise.

What have we done in this program? Let's dissect it:

  • A custom type named Lesson is defined with two required properties
  • We create a getLesson function that makes an HTTP request and returns a Promise
  • The aim is to define the data type resolved by the promise through a generic parameter on the AxiosResponse type

The goal here is to have type safety within the then block, allowing us to know that the data is of type Lesson without a type annotation.

Applying Generics to Promise Return Types

What happens? We get two compiler errors:

Error:(13, 38) TS2315: Type 'AxiosPromise' is not generic.
Error:(21, 14) TS7006: Parameter 'response' implicitly has an 'any' type.

What's the meaning behind these? Let's clarify:

  • We can't use a generic parameter to specify the data type a promise will resolve to
  • The return type of the data is implicitly considered Any
  • The promise response is also implicitly Any, causing a compilation error

The Promise type definitions included with Axios, while helpful, need a bit more type annotation work on our side to maintain safety. For instance:

In this version, we added an explicit type annotation on line 9, and it compiles without issue.

But this might make you think: Promise is a standard ES6 API. Why don't we just use that? That would give us both a standard interface and type inference.

So if this were a Node program, how could that be done?

Building Node Programs with the Standard Promise API

Our approach would be to use a library that, instead of shipping its own promise types, returns types that are compliant with ES6 promises.

Let's set up request-promise, which adds promise support to the well-known request Node HTTP client.

So, how can we apply this client to write type-safe, ES6-Promise-based programs?

Using Node's require Function in TypeScript

The request-promise library mirrors request's API but returns promises. We can start using it the same way we would in a Node module, using require.

If we write our code this way, we’ll hit this error:

Error:(3, 12) TS2304:Cannot find name 'require'.

What's happening is that we have no type definition for this global require function.

The Node runtime itself does not include type definitions, so we must fetch them separately from npm.

To install the Node runtime types, we use this command:

What did this do? What exactly is this @types module?

Understanding @types: Usage and Rationale

The @types scoped package is a treasure trove of useful typings, including the Node ones we just installed that allow us to use require.

If you've been following TypeScript for a while, you might wonder about the old days of DefinitelyTyped and the typings executable we used to install these types.

The Fate of the typings Tool and DefinitelyTyped

If you look at the npm page for the @types scope, you'll find the answer to that—@types.

As you'll see, all the old DefinitelyTyped content is now available under @types, meaning the typings tool is no longer needed for installing definitions.

You can just use npm directly, and the TypeScript compiler will automatically pick up any types found in the node_modules/@types folder during compilation.

When Is It Appropriate to Use @types?

The @types scope offers typings for a huge array of libraries, such as Express, Sequelize, jQuery, and more. So it's definitely the first place to look when you're short on types, but be sure to check two things beforehand:

  • See if the package itself already contains built-in types, and if so, opt for those.
  • Verify whether the types are already shipped with the compiler, which we'll discuss later.

While the definitions in @types are extremely useful, not all of them are the best choice for every situation. Let's illustrate this with Promises.

Using Request Promise for Type-Safe Promise Calls

Let's go ahead and install the request-promise type definitions from @types. If you're following along and have Axios installed, it's a good time to remove it to avoid conflicts.

With these type definitions in place, here's the program we want to write.

We're currently facing this error:

Error:(11, 38) TS2304: Cannot find name 'Promise'.

The Widespread "Cannot Find Promise" Problem

It seems the Node runtime type definitions we installed don't include Promises. Let's explore @types to find a suitable solution.

While the conclusions section has the full story, for now let's say we'll install Promise type definitions from @types:

This installs type definitions for ES6 promises, which include a generic parameter for the Promise type.

With this, we could say the function returns a Promise of Lesson, and the compiler would correctly infer the lesson variable's type as Lesson.

That fulfills our objective, but there's a big catch. It perfectly demonstrates why you shouldn't install types from @types indiscriminately, but rather choose them strategically.

The Hidden Issue with es6-promise

To see what the problem is, let's write a program that we expect to be flagged as an error:

Do you see the issue? The function returns a Promise of string because the lesson is converted to a string in the then clause.

Yet, the program compiles without error. So what's behind this?

Not Every Type Definition Fully Utilizes the Type System

As we saw earlier, not all type definitions exploit the full power of the TypeScript type system. This is partly because the compiler is evolving so fast that many definitions don't yet use all its latest capabilities.

This can be a blessing, though. Not everyone wants generics everywhere. An API returning Any and expecting the caller to add a type annotation is a valid design choice.

But here, using Promise with a generic parameter would be perfect. It makes complete sense to dictate what data a promise should resolve to and leverage that knowledge for type checking.

Is there a solution? It happens that the TypeScript compiler itself bundles a wealth of type definitions, and Promises are among them.

What Are Compiler Opt-In Types and When to Use Them?

Take a look at the compiler options, particularly the --lib flag here.

A huge number of type definitions are included with the compiler itself, covering everything in ES6, including Promises.

We can just tap into that by setting the lib compiler flag:

This tells the compiler about the Promise type included with it. So, if we compile our program with this enabled, what's the result?

We'll initially get a few errors:

node_modules/@types/es6-promise/index.d.ts(42,19): error TS2300: Duplicate identifier 'Promise'.
node_modules/typescript/lib/lib.es2015.iterable.d.ts(145,11): error TS2300:     Duplicate identifier 'Promise'.
...

Why Does the 'duplicate type definition' Keep Appearing?

When this error occurs, it's because a type called Promise is defined in two places:

  • one from @types/es6-promise
  • another from the compiler's built-in types you just opted into

To resolve this duplication, we can simply uninstall the Promise types from @types:

With that package removed, we are left with just these errors:

test.ts(11,12): error TS2322: Type 'Bluebird<any>' is not assignable to type 'Promise<Lesson>'.
Property '[Symbol.toStringTag]' is missing in type 'Bluebird<any>'.

Make sure to check the conclusion section for a summary of this entire situation.

It turns out that @types/request-promise actually didn't rely on external Promise types after all—its types are internally dependent on Bluebird type definitions (a powerful promise library used by Sequelize, for instance).

What can we do now? The type definitions from @types/request-promise are currently at odds with the ES6 built-in types we tried to use.

This is likely just a temporary situation, as Bluebird promises have historically been compatible with ES6 promises. By the time you're reading this, the issue has probably been resolved.

The Takeaway from this Example

This illustrates that using the absolute latest type definitions isn't always feasible or optimal at any given moment. It's an option to weigh carefully, as other approaches may be better.

Sometimes it's far more practical to stick with simpler, ready-made types like AxiosPromise, or the returns of request-promise, and just add explicit types in the places where these APIs fall back to Any.

Even if an API returns an explicit any, it doesn’t mean our own program’s types should be any too.

A good general approach is to pick packages with built-in types and lean on type inference. Import third-party types only when necessary, and in doing so, think carefully about what fits best.

What the Error We Expected Actually Looks Like

Let's check if TypeScript can catch the error we're looking to trigger, using a much simpler example from a GitHub issue. What about something like this?

This will produce the anticipated error:

test.ts(31,16): error TS2322: Type 'Promise<string>' is not assignable to type 'Promise<Foo>'.
  Type 'string' is not assignable to type 'Foo'.

So, the compiler is ready and able to catch this. It's just that the library type definitions available to us need time to evolve and adopt these features, meaning there will often be a lag.

Bridging the Gap Between Libraries and the Compiler

TypeScript uses its latest type checks on all definitions found in node_modules, including those from @types.

To prevent this, especially when dealing with incompatible legacy definitions, you can set the skipLibCheck flag to true.

This option stops the modern compiler from throwing errors against older, unmaintained libraries and also speeds up your build noticeably.

There we go—it was a ride, but these are the kinds of real-world issues you meet daily when working with TypeScript.

Let's bring it all together with some practical recommendations for using the type system effectively.

To learn TypeScript practically by building a small Express REST API that queries a SQL database with Sequelize (in a type-safe way), check out our Complete Typescript Course.

Typescript Course

Conclusions and Recommendations

When you're in a TypeScript project, you have multiple sources for type definitions. Knowing which one to use and why is key to enjoying a good developer experience.

Keep in mind: the compiler always pushes forward with more features, even when the library ecosystem hasn't caught up yet.

When Should We Go with Compiler Opt-In Types?

There's a lot to gain from using the compiler's built-in types (via the lib flag), as these are primarily designed to increase type safety and encourage usage of standardized APIs.

Still, its effectiveness depends on how the existing libraries measure up and could lead to other compatibility issues. So, when toggling this on, be sure to check for clashes with types you've already imported, like es6-promise. Using skipLibCheck can help isolate your code from potential third-party type conflicts.

However, don't feel pressured to use opt-in types. They're off by default for a reason—primarily to maintain backwards compatibility with a substantial portion of the ecosystem.

Guidelines on When to Use @types?

Instead of making @types your default, try to use whatever types are bundled with each module. Consider @types only as a strategic resource, particularly when working with frameworks like Express or Sequelize.

Older, more traditional JavaScript libraries like those will likely constitute most of what you use and have top-tier typings on @types. On the other hand, newer tools—such as Firebase—now include their own types.

In that case, installing @types/firebase would cause a duplicate type conflict.

Meanwhile, packages like @types/node are simply necessary to write any Node.js application.

The suggestion, then, is to look at the built-in types for what you need, review the module to see if it already contains types (which will happen more and more often), and only then browse @types if you're missing something.

In an ideal future, there would be no @types, as libraries would all come with their own type definitions.

But we're not there just yet, and it's extremely valuable to have these high-quality types readily available for now.

What If There Are No Types At All?

A library without type definitions doesn't block us. Starting with TypeScript 2.1, if a module is available in node_modules and lacks types, we can't import it directly without issue. When we do, the import is silently given the Any type.

These details make it simple to mix any JavaScript module into your TypeScript codebase without a special setup.

Getting the Most Type Safety in Your Programs

One of the smartest moves outside of carefully choosing your types is to turn noImplicitAny to true.

The outcome is that the compiler will be able to infer just about every type in your application, with minimal explicit annotations needed on your part—mostly in function arguments, which is good practice to do from the start.

I hope this guide was helpful. If you’d like more useful posts and resources on Angular, take a look at the links below.

Sign up for our newsletter to be the first to know about new posts:

And if you're just starting with Angular, consider the Angular for Beginners Course:

Typescript Typings: The Complete Guide: @types Compiler Opt-In Types — figure 2

Other Angular Posts

If you enjoyed this article, you might also find these other popular posts interesting: