Declaration file

When using a third-party library, we need to reference its declaration file to get the corresponding code completion, interface prompts and other func

What is a declaration statement

If we want to use the third-party library jQuery, a common way is to introduce jQuery through the tag in html, and then we can use global variables $ or jQuery. We usually get an element whose id is bar like this:

$('#bar');
// or
jQuery('#bar');

But in ts, the compiler does not know what $ or jQuery is :

jQuery('#bar');
// ERROR: Cannot find name 'jQuery'.

At this time, we need to use declare var to define its type

declare var jQuery: (selector: string) => any
jQuery('#bar')

In the above example, declare var does not really define a variable, but only defines the type of the global variable jQuery, which will only be used for compile-time checks and will be deleted in the compilation result. The result of its compilation is:

What is a declaration file Usually we put the declaration statement in a separate file (jQuery.d.ts), this is the declaration file

// src/jQuery.d.ts
declare var jQuery: (selector: string) => any;

// src/index.ts
jQuery('#bar');

The declaration file must be suffixed with .d.ts. Generally speaking, ts will parse all .ts files in the project, of course, it also includes files ending in .d.ts. So when we put jQuery.d.ts into the project, all other .ts files can get the type definition of jQuery.

/path/to/project
├── src
|  ├── index.ts
|  └── jQuery.d.ts
└── tsconfig.json

If it still cannot be parsed, you can check the files, include, and exclude configurations in tsconfig.json to ensure that it contains the jQuery.d.ts file. Only the declaration file of the global variable mode is demonstrated here. If the third-party library is used through module import, then the introduction of the declaration file is another way, which will be described in detail later.

Third-party declaration file

Of course, the jQuery declaration file does not need to be defined by us. The community has already defined it for us: jQuery in DefinitelyTyped. We can download it and use it directly, but it is more recommended to use @types to manage the declaration files of third-party libraries in a unified way. The way to use @types is very simple, just use npm to install the corresponding declaration module. Take jQuery as an example:

npm install @types/jquery --save-dev

Write declaration document In different scenarios, the content and usage of the declaration file will be different The main usage scenarios of the library are as follows:

  • Global variables: Introduce third-party libraries through tag, and inject global variables
  • npm package: Import through import bar from 'bar', conforming to ES6 module specifications

Global variable

Global variables are the simplest scenario. The previous example is to introduce jQuery through the tag, and inject global variables $ and jQuery.

When using the declaration file of global variables, if it is installed with npm install @types/xxx --save-dev, no configuration is required. If you store the declaration file directly in the current project, it is recommended to put it in the src directory (or the corresponding source code directory) together with other source code:

/path/to/project
├── src
|  ├── index.ts
|  └── jQuery.d.ts
└── tsconfig.json

If it does not take effect, you can check the files, include, and exclude configurations in tsconfig.json to ensure that it contains the jQuery.d.ts file.

The declaration files of global variables mainly have the following syntaxes:

  • declare var Declare global variables
  • declare function Declare a global method
  • declare class Declare a global class
  • declare enum Declare the global enumeration type

declare var

In all declaration statements, declare var is the simplest. As you learned before, it can be used to define the type of a global variable. Similar to it, there are declare let and declare const. There is no difference between using let and var:

// src/jQuery.d.ts

declare let jQuery: (selector: string) => any;
// src/index.ts
jQuery('#bar');
// Use the jQuery type defined by declare let to allow modification of this global variable
jQuery = function(selector) {
return document.querySelector(selector);
};

When we use the const definition, it means that the global variable at this time is a constant and it is not allowed to modify its value

// src/jQuery.d.ts

declare const jQuery: (selector: string) => any;

jQuery('#bar');
// Using the jQuery type defined by declare const, it is forbidden to modify this global variable
jQuery = function(selector) {
    return document.querySelector(selector);
};
// ERROR: Cannot assign to 'jQuery' because it is a constant or a read-only property.

Generally speaking, global variables are constants that are forbidden to be modified, so in most cases, you should use const instead of var or let. It should be noted that only the type can be defined in the declaration statement, and the specific implementation must not be defined in the declaration statement

declare const jQuery = function(selector) {
    return document.querySelector(selector);
};
// ERROR: An implementation cannot be declared in ambient contexts.

declare function

declare function is used to define the type of global function. jQuery is actually a function, so it can also be defined by function:

// src/jQuery.d.ts

declare function jQuery(selector: string): any;
// src/index.ts

jQuery('#bar');

declare class When the global variable is a class, we use declare class to define its type:

// src/Animal.d.ts

declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string;
}

// src/index.ts

let cat = new Animal('Tom');

Similarly, the declare class statement can only be used to define types, not to define specific implementations. For example, when defining specific implementations of the sayHi method, an error will be reported:

// src/Animal.d.ts

declare class Animal {
    name: string;
    constructor(name: string);
    sayHi() {
        return `My name is ${this.name}`;
    };
    // ERROR: An implementation cannot be declared in ambient contexts.
}

declare enum The enumeration type defined by declare enum is also called external enumeration (Ambient Enums), for example, as follows

// src/Directions.d.ts

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
// src/index.ts

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

Consistent with the type declarations of other global variables, declare enum is only used to define types, not specific values. Directions.d.ts will only be used for compile-time checking, and the content in the declaration file will be deleted in the compilation result. The result of its compilation .

declare enum The enumeration type defined by declare enum is also called external enumeration (Ambient Enums). Examples are as follows

// src/Directions.d.ts

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

// src/index.ts

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

Consistent with the type declaration of other global variables, declare enum is only used to define the type, not the specific value.

Directions.d.ts will only be used for compile-time checking, and the content in the declaration file will be deleted in the compilation result. The result of its compilation is:

var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

terface and type

In addition to global variables, there may be some types that we also want to expose. In the type declaration file, we can directly use interface or type to declare a global interface or type 12:

// src/jQuery.d.ts

interface AjaxSettings {
    method?: 'GET' | 'POST'
    data?: any;
}
declare namespace jQuery {
    function ajax(url: string, settings?: AjaxSettings): void;
}

In this case, this interface or type can also be used in other files:

let settings: AjaxSettings = {
    method: 'POST',
    data: {
        name: 'bar'
    }
};
jQuery.ajax('/api/post_something', settings);

Prevent naming conflicts The interface or type exposed at the outermost layer will act as a global type in the entire project. We should reduce the number of global variables or global types as much as possible. So it is best to put them under namespace

// src/jQuery.d.ts
declare namespace jQuery {
    interface AjaxSettings {
        method?: 'GET' | 'POST'
        data?: any;
    }
    function ajax(url: string, settings?: AjaxSettings): void;
}

// src/index.ts

let settings: jQuery.AjaxSettings = {
    method: 'POST',
    data: {
        name: 'bar'
    }
};
jQuery.ajax('/api/post_something', settings);

Declaration merge If jQuery is both a function and can be called directly jQuery('#bar'), it is also an object with sub-attributes jQuery.ajax() (this is indeed the case), then we can combine multiple declaration statements and they will not conflict The combined 14:

// src/jQuery.d.ts

declare function jQuery(selector: string): any;
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
}
// src/index.ts

jQuery('#bar');
jQuery.ajax('/api/get_something');

npm package Generally, we import an npm package by import bar from 'bar', which conforms to the ES6 module specification. Before we try to create a declaration file for an npm package, we need to check whether its declaration file already exists. Generally speaking, the declaration file of the npm package may exist in two places:

  1. it is bound with the npm package. The basis for judgment is that there is a type field in package.json, or there is an index.d.ts declaration file. This mode does not require additional installation of other packages and is the most recommended, so when we create the npm package ourselves in the future, it is best to also bind the declaration file with the npm package.
  2. Publish to @types. We only need to try to install the corresponding @types package to know if the declaration file exists. The installation command is npm install @types/bar--save-dev. This mode is generally because the maintainer of the npm package does not provide a declaration file, so other people can only publish the declaration file to @types.

If the corresponding declaration file is not found in the above two methods, then we need to write a declaration file for it by ourselves. Since it is a module imported through the import statement, the storage location of the declaration file is also restricted. Generally, there are two solutions:

  1. create a node_modules/@types/bar/index.d.ts file and store the declaration file of the bar module. This method does not require additional configuration, but the node_modules directory is unstable, the code is not saved in the warehouse, and the version cannot be traced back, and there is a risk of accidental deletion. Therefore, this solution is not recommended, and it is generally only used as a temporary test.
  2. Create a types directory to manage the declaration files written by yourself, and put the declaration files of bar in types/bar/index.d.ts. In this way, you need to configure the paths and baseUrl fields in tsconfig.json.

Directory Structure:

/path/to/project
├── src
|  └── index.ts
├── types
|  └── bar
|     └── index.d.ts
└── tsconfig.json

tsconfig.json content:

{
    "compilerOptions": {
        "module": "commonjs",
        "baseUrl": "./",
        "paths": {
            "*": ["types/*"]
        }
    }
}

After this configuration, when bar is imported through import, the declaration file of the corresponding module will also be found in the types directory. Note that the module configuration can have many options, and different options will affect the import and export modes of the module. Here we use commonjs, the most commonly used option, and the following tutorials will also use this option by default. No matter which of the above two methods is used, I strongly recommend that you publish the written declaration file (by sending a pull request to a third-party library, or directly submitting it to @types) to the open source community, and enjoy With so many excellent resources from the community, you should give some feedback whenever you can. Only when everyone is involved can the ts community be more prosperous.

The declaration files of the npm package mainly have the following syntaxes:

  • export export variables export namespace
  • export objects (containing sub-attributes)
  • export default ES6 default
  • export export = commonjs
  • export module

export The declaration file of the npm package is quite different from the declaration file of global variables. In the declaration file of the npm package, using declare will no longer declare a global variable, but only a local variable in the current file. These types of declarations will only be applied to these types of declarations only when export is used in the declaration file, and then imported by the user. The grammar of export is similar to the grammar in ordinary ts, except that the specific implementation is prohibited from being defined in the declaration file.

// types/bar/index.d.ts

export const name: string;
export function getName(): string;
export class Animal {
    constructor(name: string);
    sayHi(): string;
}
export enum Directions {
    Up,
    Down,
    Left,
    Right
}
export interface Options {
    data: any;
}

The corresponding import and use module should look like this:

// src/index.ts

import { name, getName, Animal, Directions, Options } from 'bar';

console.log(name);
let myName = getName();
let cat = new Animal('Tom');
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
let options: Options = {
    data: {
        name: 'bar'
    }
};

Mixed use of declare and export We can also use declare to declare multiple variables first, and then export them all at once. The declaration file of the above example can be equivalently rewritten as

// types/bar/index.d.ts

declare const name: string;
declare function getName(): string;
declare class Animal {
    constructor(name: string);
    sayHi(): string;
}
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
interface Options {
    data: any;
}

export { name, getName, Animal, Directions, Options };

Note that, similar to the declaration file of global variables, there is no need to declare before the interface.

export namespace Similar to declare namespace, export namespace is used to export an object with sub-attributes

// types/bar/index.d.ts

export namespace bar {
    const name: string;
    namespace subBar {
        function baz(): string;
    }
}
// src/index.ts

import {bar } from 'bar';

console.log(bar.name);
bar.subBar.baz();

export default In the ES6 module system, you can use export default to export a default value, and the user can use import bar from 'bar' instead of import {bar} from ' bar' to import the default value

In the type declaration file, export default is used to export the type of the default value

// types/bar/index.d.ts

export default function bar(): string;

// src/index.ts

import bar from 'bar';
bar();

Note that only function, class and interface can be directly exported by default, and other variables need to be defined first, and then exported by default:

// types/foo/index.d.ts

export default enum Directions {
// ERROR: Expression expected.
    Up,
    Down,
    Left,
    Right
}

In the above example, export default enum is the wrong syntax. You need to use declare enum to define it, and then use export default to export:

// types/bar/index.d.ts

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

export default Directions;

export = In the commonjs specification, we export a module in the following way:

// Overall export
module.exports =foo;
// Single export
exports.bar = bar;

In ts, there are many ways to import this kind of module export. The first way is const ... = require:

// Import as a whole
const foo = require('foo');
// single import
const bar = require('foo').bar;

The second way is import ... from. Note that for the overall export, you need to use import * as to import:

// Import as a whole
import * as foo from'foo';
// single import
import {bar} from'foo';

The third way is import ... require, which is also officially recommended by ts:

// Import as a whole
import foo = require('foo');
// single import
import bar = foo.bar;

For this kind of library that uses the commonjs specification, if you want to write a type declaration file for it, you need to use the export = syntax:

// types/foo/index.d.ts

export = foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}

It should be noted that after export = is used in the above example, export {bar} can no longer be exported individually. So we merged by declaration, using declare namespace foo to merge bar into foo. To be precise, export = can be used not only in declaration files, but also in ordinary ts files. In fact, import... require and export = are both new grammars created by ts to be compatible with AMD and commonjs specifications. Since they are not commonly used or recommended, they will not be introduced in detail here. If you are interested, you can see the official Document. Since many third-party libraries are standardized by commonjs, the declaration file has to use export = syntax. But it still needs to be emphasized that, compared with export =, we recommend the use of ES6 standard export default and export.