Javarevisited
Published in

Javarevisited

Role of Declaration Files in the Implementation of TypeScript

In this article, we will look at how we can enhance a JavaScript library and add syntactic sugar through declaration files.

One of the most appealing facets of JavaScript development is the wealth of external JavaScript libraries that have already been published, are tried and tested, and are available for re-use.

This article is an excerpt from the book, Mastering TypeScript, 4th Edition by Nathan Rozentals — A comprehensive guide to understand TypeScript language and its latest features.

Declaration files

A declaration file is a special type of file used by the TypeScript compiler. It is only used during the compilation step and is used as a sort of reference file to describe JavaScript. Declaration files are similar to the header files used in C or C++ or the interfaces used in Java. They simply describe the structure of available functions and properties but do not provide an implementation. In this section of the chapter, we will take a look at these declaration files, what they are, and how to write them.

Declaration file typing

As we know, declaration files use the declare and module keywords to define objects and namespaces. We have also seen that we can use interfaces in the same way that we do within TypeScript, in order to define custom types for variables. Declaration files allow us to use the same syntax that we would in TypeScript to describe types. These types can be used everywhere types are used in normal TypeScript, including function overloading, type unions, classes, and optional properties. Let’s take a quick look at these techniques, with a few simple code samples, to illustrate this feature further. This section will cover:

  • Function overloading
  • Nested namespaces
  • Classes
  • Static properties and functions
  • Abstract classes
  • Generics
  • Conditional types and inference

We’ll begin by taking a look at function overloading.

Function overloading

Declaration files allow for function overloads, where the same function can be declared with different arguments, as follows:

declare function trace(arg: string | number | boolean);
declare function trace(arg: { id: number; name: string });

Here, we have a function named trace that is declared twice: once with a single parameter named arg, of type string or number or boolean, and once with the same arg parameter, which is a custom type. This overloaded declaration allows for all of the following valid code:

trace(“trace with string”);
trace(true);
trace(1);
trace({ id: 1, name: “test” });

Here, we have exercised the various combinations of arguments that are allowed by our function overrides.

Nested namespaces

Declaration files allow for module names to be nested. This in turn translates to nested namespaces, as follows:

declare module FirstNamespace {
module SecondNamespace {
module ThirdNamespace {
function log(msg: string);
}
}
}

Here, we have declared a module named FirstNamespace that exposes a module named SecondNamespace, which in turn exposes a third module named ThirdNamespace. The ThirdNamespace module defines a function named log. This declaration will result in all three namespaces needing to referenced in order to call the log function, as follows:

FirstNamespace.SecondNamespace.ThirdNamespace.log(“test”);

Here, we are explicitly referencing each named namespace in order to call the log function within the ThirdNamespace module.

Classes

Class definitions are specified in module definitions using the class keyword, as it would be in normal TypeScript files, as follows:

declare class MyModuleClass {
public print(): void;
}

Here, we have declared a class named MyModuleClass that has a public print function that returns void. This class definition can then be used as follows:

let myClass = new MyModuleClass();
myClass.print();

Here, we are creating an instance of the MyModuleClass class that has been declared in our declaration file. We then are calling the print function of the class instance named myClass.

Declaring a class in a declaration file is very similar to defining an interface for it. We do not provide any implementations of the functions, we are only declaring to the TypeScript compiler that the class exists and what properties and functions are available to it.

Static properties and functions

In the same manner that we can mark a property or function as static in TypeScript, we can use the same syntax in a declaration file, as follows:

declare class MyModuleStatic {
static print(): void;
static id: number;
}

Here, we have declared a class named MyModuleStatic that has a static print function and a static id property. We can use these static properties and functions as follows:

MyModuleStatic.id = 10;
MyModuleStatic.print();

Here, we can see that the declaration of a static function or property follows the same usage rules as if we had defined it in TypeScript.

Abstract classes

Declaration files can define abstract classes and functions as follows:

declare abstract class MyModuleAbstract {
abstract print(): void
}

Here, we have defined an abstract class named MyModuleAbstract that has a single function named print that has also been marked as abstract. We can use this abstract class declaration in a TypeScript file as follows:

class DerivedFromAbstract extends MyModuleAbstract {
print() { }
}

Here, we have defined a class named DerivedFromAbstract that extends the MyModuleAbstract class from our declaration file.

Note that we will also need to provide an implementation of the print function within this class definition, as the print function has been marked as an abstract function in the class declaration.

Generics

Declaration files allow generic syntax to be used, as follows:

declare function sort<T extends number | string>
(input: Array<T>): Array<T> { }

Here, we are declaring a function named sort that is using generic syntax to specify the type of T to be either a number or a string, or both. This sort function has a single parameter named input that is an array of type T and returns an array of type T. This declaration will allow the following usage:

let sortedStringArray = sort([“first”, “second”]);
let sortedNumericArray = sort([1, 2, 3]);

Here, we have defined a variable named sortedStringArray to hold the return value of a call to the sort function, where we pass in an array of strings as the only argument. The type of the sortedStringArray variable will be Array<string>. Next, we define a variable named sortedNumericArray to hold the return value of another call to the sort function, where we pass in an array of numbers as the only argument. The sortedNumericArray variable will be of type Array<number>.

Conditional types

Declaration files can also define conditional types and distributed conditional types, as follows:

declare type stringOrNumberOrBoolean<T> =
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean : never;

Here, we have declared a type named stringOrNumberOrBoolean that is using generic syntax to define a type of T. If T extends string, then the type will be string. If T extends number, then the type will be number. If the type of T extends boolean, then the type will be boolean. If T does not extend any of these types, then the type will be never.

We can now use this distributed conditional type as follows:

type myNever = stringOrNumberOrBoolean<[string,number]>;

Here, we have defined a type named myNever that is the result of the distributed conditional type named stringOrNumberOrBoolean, and has defined a type of T as a tuple of string and number. As this tuple does not match any of the types we are checking for, the type of the myNever variable will be never.

Conditional type inference

Declaration files allow for conditional type inference, as can be seen from the following example:

declare type inferFromPropertyType<T> =
T extends { id: infer U } ? U : never;

Here, we have a type named inferFromPropertyType that will infer the type named U from the property named id of the type T. If the id property does not exist, then the type will be never. We can now use this type as follows:

type myString = inferFromPropertyType<{ id: string }>;
type myNumber = inferFromPropertyType<{ id: number }>;

Here, we have defined two types, named myString and myNumber, that are using the inferred conditional type named inferFromPropertyType. As the type of the id property is a string in the first line, then myString will be of type string. The type of the id property in the second line of code is number, and therefore myNumber will be of type number.

Declaration file summary

What we have seen in this article is that the type rules and typing techniques that TypeScript provides can all be used within declaration files. The purpose of a declaration file is to tell the TypeScript compiler ahead of time what the structure of a JavaScript library looks like. We have seen that we can use all of the TypeScript keywords and language features within a declaration file.

About the author

Nathan Rozentals has been writing commercial software for over 30 years, in C, C++, Java, and C#. He picked up TypeScript within a week after its initial release in October 2012 and realized how much TypeScript could help when writing JavaScript.

Nathan’s TypeScript solutions now control User Interfaces in IoT devices, run as stand-alone applications for Point-of-Sale solutions, provide complex application configuration websites, and are used for mission-critical server APIs.

This article is an excerpt from the book, Mastering TypeScript, 4th Edition by Nathan Rozentals — A comprehensive guide to understand TypeScript language and its latest features. If you like the article then you will love the book too.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Packt

Packt

3.4K Followers

We help developers build better software | Email customercare@packtpub.com for support | Twitter support 9-5 Mon-Fri