References
I learnt the concept of “ambient” in TypeScript from these two references. Read them if you wish, or just read my understanding below.
- This GitHub issue comment provides a definition of “ambient” in TypeScript
- Typescript ambient module declarations
Ambient Variable Declaration
Assuming you’re writing a TypeScript code that depends on jquery available on global (included via script
tag), how do you do that?
$('.btn').toggleClass('active');
Use the declare
keyword:
declare var var $: (selector: string) => {
toggleClass: (className: string) => void;
}
$: {
(selector: string
selector: string): {
toggleClass: (className: string) => void
toggleClass: (className: string
className: string) => void;
};
};
var $: (selector: string) => {
toggleClass: (className: string) => void;
}
$('.btn').toggleClass: (className: string) => void
toggleClass('active');
The declare
keyword is known as ambient declaration, which means you declare the typing of a variable without implementation, and TypeScript will just assume you will somehow include that implementation yourself (via adding a script
tag or inject via webpack, or whatever custom compiler you have).
The concept of ambient is not used within a file to declare a variable (like the $
in the example above), it is also used in the context of ES Module and third-party packages.
Let’s look at them.
Declare the Typing of ES Module
A ES Module is just a fancy way to describe a JavaScript file that use import
to specify its dependencies and export
to specify what it expose.
For an example, you have the following ES Module (a file in your project):
export const sum = (a, b) => a + b;
To import it in TypeScript file, TypeScript will yell at you (unless you opt out this check by setting compilerOptions.allowJs
to true
in tsconfig.json
):
import { sum } from './math'; // TypeScript Error: no declaration bla bla bla
console.log(sum(4, 5));
To fix it, create a math.d.ts
:
export function sum(a: number, b: number): number;
And TypeScript will be happy now.
This math.d.ts
file is known as ambient module (module without implementation). It is like the declare
keyword, with the difference that declare
works for a variable within the file while .d.ts
works for a module. When you name a file as <something>.d.ts
, TypeScript will automatically associate it with the file <something>.js
. Similar to declare
keywords, TypeScript will assume your ambient declaration is accurate and reflect the actual implementation.
Declare Typing of Third Party Library
What about the third-party code in node_modules
that is not TypeScript (which is all of them because nobody publish TypeScript files)?
Few possibilities:
-
The third-party package may includes the typing itself (by providing an
index.d.ts
file at the root of the package, or specified usingtypings
in itspackage.json
). In this case, TypeScript will able to associate the ambient declaration without you doing any work. Example:reselect
use thetypings
field whileaxios
put theindex.d.ts
at its root.. -
If the third-party package does not include typing, you may be able to find type definition contributed by others from DefinitelyTyped, which publish its package under
@types
scope. For instance,react-router-dom
does not comes with typing definition and you can install the typing withnpm install @types/react-router-dom
. By default, if TypeScript could not find typing definition in the package folder, it will try to resolve it innode_modules/@types
folder. -
If unfortunately nobody contribute the typing of the package you use to DefinitelyTyped, the last resort is to declare it yourself. To do that you need to create an ambient module declaration. An ambient module declaration has the following format and must follow the naming
.d.ts
.declare module 'my-utils' { export const a: number; export function doX(a: boolean): void; }
The confusing part here is that compared to the
math.d.ts
example above, there is no any special requirement of the file name here except the it must ends with.d.ts
. As long as thexyz.d.ts
file above is included in the compilation, TypeScript will register that there’s a module namedmy-utils
which then can be imported with:import { a, doX } from 'my-utils';
But how do we include
xyz.d.ts
? Few ways:- Specify path in
compilerOptions.typeRoots
intsconfig.json
. - Specify
files
intsconfig.json
. - Use the triple slash directive
/// <reference path="..." />
- Specify path in