本节介绍声明文件相关内容,包括声明文件的规范、原理、结构、模板、发布及使用等相关内容。
讲解视频
TS学习笔记二十一:声明文件结构介绍
TS学习笔记二十四:声明文件示例
TS学习笔记二十二:声明文件规范
TS学习笔记二十三:声明文件原理
B站视频
TS学习笔记二十一:声明文件结构介绍
西瓜视频
https://www.ixigua.com/7325237297077715497
编写声明文件之前需要识别库的类型,包括全局库、模块化库和UMD库。
全局库是指能在全局命名空间下访问的,不需要任何形式的import,如Jquery等,只简单的暴露出一个或多个全局变量。全局库直接使用script标签引用。
识别全局库时,通常会看到:
在全局库的源码中无法看到:
var a = require(‘a’);
全局库可以很轻易的转换为UMD库,所以目前大多数库都是UMD类型的,全局库的声明模板文件是global.d.ts,具体内容在后面章节。
模块化库只能工作在有模块加载器的环境中,如expres只能在node.js中工作,使用时必须使用require函数加载。
模块库至少会包含下列条件之一:
极少包含window或global对象的赋值。
UMD模块是既可以作为模块使用,也可以作为全局使用的模块。UMD模块会自动检查是否存在模块加载器环境,如果在库中看到了typeof define,typeof window或typeof module等时几乎就可以确定是一个UMD库了。
模块有三个可用模板:module.d.ts, module-class.d.ts, module-function.d.ts。
如果模块可以被当做函数调用,则使用module-function.d.ts;如果模块能够使用new来构建,则使用module-class.d.ts;如果不能当做函数调用也不能用new来构建,则使用module.d.ts。
一个模块插件可以改变一个模块的机构,对于声明文件的目标,可以写相同的代码不论被改变的模块是一个纯粹的模块还是UMD模块,模板为module-plugin-d-ts。
全局插件:
一个全局插件是全局代码,会改变全局对象的结构,对于全局修改的模块,在运行时存在冲突的可能。可以从文档中看出是否是一个全局插件。使用global-plugin-d-ts模板。
全局修改的模块:
当一个全局修改的模块被导入的时候,会改变全局作用域里的值,可能造成运行时的冲突。可以从文档中识别出来,与全局插件相似,使用global-modifying-module-d-ts模板。
如果库依赖其它的库,可能会有以下几种依赖:
1. 依赖全局库
/// 指令:
///
function getThing(): someLib.thing;
2. 依赖模块
使用import引入:
import * as moment from "moment";
function getThing(): moment;
3. 依赖UMD库
如果全局库依赖于某个UMD模块,使用///
///
function getThing(): moment;
如果模块或UMD依赖于一个UMD库,使用import语句:
import * as someLib from 'someLib';
不能使用///指令声明UMD库的依赖。
书写全局声明文件时,可能会导致无法处理的命名冲突,可以使用库定义的全局变量名来声明命名空间类型。
一个插件添加或修改已存在的顶层模块的导出部分,在Commonjs或其他加载器里是允许的,ES模块被当做时不可改变的,因此此种模式就不可行,因为ts是能不预知加载器类的,所以没在编译时保证。
在ES6模块加载器中,顶层的对象只能具有属性,顶层的模块对象永远不能被调用。
对于普通类型number/string/boolean在写声明文件时不要使用对应的装盒对象,即Number、String、和Boolean,注意使用首字母小写的原始类型,如果要用Object,使用any代替。
不要定义一个从来没有使用过其类型参数的泛型类型。
回调函数返回值类型:
不要为返回值忽略的回调函数设置一个any返回值类型,应该给返回值类型被忽略的回调函数设置void类型的返回值类型:
/* 错误 */
function fn(x: () => any) {
x();
}
/* OK */
function fn(x: () => void) {
x();
}
因为使用void类型相对安全,可以防止不小心使用了x的返回值:
function fn(x: () => void) {
var k = x(); // oops! meant to do something else
k.doSomething(); //此处使用了x的返回值,有可能报错。
}
回调函数里的可选参数:
不要在回调函数里使用可选参数:
/* 错误 */
interface Fetcher {
getObject(done: (data: any, elapsedTime?: number) => void): void;
}
上述实例中的done回调函数可能以1个参数或2个参数进行调用,不需要把这个参数当成可选参数来处理,允许提供一个接收较少参数的回调函数:
/* OK */
interface Fetcher {
getObject(done: (data: any, elapsedTime: number) => void): void;
}
重载与回调函数:
不要因为回调函数参数个数不同而写不同的重载,应该只使用最大参数个数写一个重载,因为回调函数总是可以忽略某个参数的,因此没有必要为参数少的情况写重载,参数少的回调函数首先允许错误类型的函数被传入,因为他们匹配第一个重载:
/* 错误 */
declare function beforeAll(action: () => void, timeout?: number): void;
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;
/* OK */
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;
顺序:
不要把一般的重载放在精确的重载前面,应该排序重载令精确的排在一般的签名,因为当解析函数调用的时候ts会选择第一个匹配到的重载,当前面的重载比后面的普通,则后面的将会被隐藏了不会被调用:
/* 错误 */
declare function fn(x: any): any;
declare function fn(x: HTMLElement): number;
declare function fn(x: HTMLDivElement): string;
var myElem: HTMLDivElement;
var x = fn(myElem); // x: any, wat?
/* OK */
declare function fn(x: HTMLDivElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: any): any;
var myElem: HTMLDivElement;
var x = fn(myElem); // x: string, :)
使用可选参数:
不要仅在末尾参数不同时写不同的重载,应该尽可能使用可选参数:
/* 错误 */
interface Example {
diff(one: string): number;
diff(one: string, two: string): number;
diff(one: string, two: string, three: boolean): number;
}
/* OK */
interface Example {
diff(one: string, two?: string, three?: boolean): number;
}
在所有重载都有相同的返回值时会不好用,因为ts解析签名兼容性时会查看是否某个目标签名能够使用源的参数调用,且允许外来参数:
function fn(x: (a: string, b: number, c: number) => void) { }
var x: Example;
// When written with overloads, OK -- used first overload
// When written with optionals, correctly an error
fn(x.diff);
当使用了ts严格检查null特性时,因为没有指定的参数在js里标识undefined,通常显示的为可选参数传入一个undefined:
var x: Example;
// When written with overloads, incorrectly an error because of passing 'undefined' to 'string'
// When written with optionals, correctly OK
x.diff("something", true ? undefined : "hour");
使用联合类型:
不要仅在某个位置上的参数类型不同的情况下定义重载,应该尽可能使用联合类型:
/* WRONG */
interface Moment {
utcOffset(): number;
utcOffset(b: number): Moment;
utcOffset(b: string): Moment;
}
/* OK */
interface Moment {
utcOffset(): number;
utcOffset(b: number|string): Moment;
}
function fn(x: string): void;
function fn(x: number): void;
function fn(x: number|string) {
// When written with separate overloads, incorrectly an error
// When written with union types, correctly OK
return moment().utcOffset(x);
}
类型通过以下方式引入,每种声明形式都会创建一个新的类型名称:
与类型相比,值是运行时的名字,可以在表达式里引用,创建方式如下:
类型可以存在于命名空间中,如:let a : A.B.C,此时C类型来自A.B的命名空间。
一个给定的名字A,可以找出三种不同的意义,一个类型、一个值或一个命名空间,如let a : A.A = A,A首先被当做命名空间,然后作为类型名,最后是值。
声明class时会声明两个东西,如class A{},此时声明了一个类型A指向类的实例机构,值C指向类构造函数,枚举声明也有类似的行为。
可以使用组合通过相同的名字标识两种不同的对象:
export var Bar: { a: Bar };
export interface Bar {
count: number;
}
有一些声明可以通过多个声明进行组合,如class C{}和interface C{}可以同时存在并且都可以作为C类型的属性。值总是会和同名的其它值产生冲突,除非他们在不同的命名空间中,类型冲突则发生在使用类型别名声明的情况下,命名空间永远不会发生冲突。
利用interface添加:
可以使用一个interface给别的一个interface声明里添加额外的成员,但不能使用别名给类型别名中添加成员:
interface Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK
也可以作用于类:
class Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK
使用namespace添加:
命名空间声明可以用来添加新类型,值和命名空间,可以添加一个静态成员到一个类:
class C {
}
// ... elsewhere ...
namespace C {
export let x: number;
}
let y = C.x; // OK
也可以给类添加一个命名空间类型:
class C {
}
// ... elsewhere ...
namespace C {
export interface D { }
}
let y: C.D; // OK
上述实例中作为命名空间的C不会和类创建的值或类型C互相冲突。也可以合同namespace声明:
namespace X {
export interface Y { }
export class Z { }
}
// ... elsewhere ...
namespace X {
export var Y: number;
export namespace Z {
export class C { }
}
}
type X = string;
上述示例中,第一段代码创建了以下名字于含义:
一个值X(因为namespace声明包含一个值,Z)
一个命名空间X(因为namespace声明包含一个值,Z)
在命名空间X里的类型Y
在命名空间X里的类型Z(类的实例结构)
值X的一个属性值Z(类的构造函数)
第二段代码创建了以下名字和含义:
值Y(number类型),它是值X的一个属性
一个命名空间Z
值Z,它是值X的一个属性
在X.Z命名空间下的类型C
值X.Z的一个属性值C
类型X
使用export=或import:
export和import声明会导出或导入目标的所有含义。
使用declare var声明变量,如果变量是只读的,可以使用declare const,也可以使用declare let声明拥有块级作用域的变了。
使用declare function声明函数。
使用declare namespace描述用点表示法访问的类型或值。
使用declare function进行声明函数,重载时可以如下进行声明:
declare function getWidget(n: number): Widget;
declare function getWidget(s: string): Widget[];
使用interface定义一个带有属性的类型;
可以使用类型别名来定义类型的短名,
使用命名空间组织类型,也可以在一个声明中创建嵌套的命名空间。
使用declare class描述一个类或像类一样的对象,类可以有属性和方法,就像构造函数一样。
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>
/*~ This is the global-modifying module template file. You should rename it to index.d.ts
*~ and place it in a folder with the same name as the module.
*~ For example, if you were writing a file for "super-greeter", this
*~ file should be 'super-greeter/index.d.ts'
*/
/*~ Note: If your global-modifying module is callable or constructable, you'll
*~ need to combine the patterns here with those in the module-class or module-function
*~ template files
*/
declare global {
/*~ Here, declare things that go in the global namespace, or augment
*~ existing declarations in the global namespace
*/
interface String {
fancyFormat(opts: StringFormatOptions): string;
}
}
/*~ If your module exports types or values, write them as usual */
export interface StringFormatOptions {
fancinessLevel: number;
}
/*~ For example, declaring a method on the module (in addition to its global side effects) */
export function doSomething(): void;
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
export { };
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>
/*~ This template shows how to write a global plugin. */
/*~ Write a declaration for the original type and add new members.
*~ For example, this adds a 'toBinaryString' method with to overloads to
*~ the built-in number type.
*/
interface Number {
toBinaryString(opts?: MyLibrary.BinaryFormatOptions): string;
toBinaryString(callback: MyLibrary.BinaryFormatCallback, opts?: MyLibrary.BinaryFormatOptions): string;
}
/*~ If you need to declare several types, place them inside a namespace
*~ to avoid adding too many things to the global namespace.
*/
declare namespace MyLibrary {
type BinaryFormatCallback = (n: number) => string;
interface BinaryFormatOptions {
prefix?: string;
padding: number;
}
}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>
/*~ If this library is callable (e.g. can be invoked as myLib(3)),
*~ include those call signatures here.
*~ Otherwise, delete this section.
*/
declare function myLib(a: string): string;
declare function myLib(a: number): number;
/*~ If you want the name of this library to be a valid type name,
*~ you can do so here.
*~
*~ For example, this allows us to write 'var x: myLib';
*~ Be sure this actually makes sense! If it doesn't, just
*~ delete this declaration and add types inside the namespace below.
*/
interface myLib {
name: string;
length: number;
extras?: string[];
}
/*~ If your library has properties exposed on a global variable,
*~ place them here.
*~ You should also place types (interfaces and type alias) here.
*/
declare namespace myLib {
//~ We can write 'myLib.timeout = 50;'
let timeout: number;
//~ We can access 'myLib.version', but not change it
const version: string;
//~ There's some class we can create via 'let c = new myLib.Cat(42)'
//~ Or reference e.g. 'function f(c: myLib.Cat) { ... }
class Cat {
constructor(n: number);
//~ We can read 'c.age' from a 'Cat' instance
readonly age: number;
//~ We can invoke 'c.purr()' from a 'Cat' instance
purr(): void;
}
//~ We can declare a variable as
//~ 'var s: myLib.CatSettings = { weight: 5, name: "Maru" };'
interface CatSettings {
weight: number;
name: string;
tailLength?: number;
}
//~ We can write 'const v: myLib.VetID = 42;'
//~ or 'const v: myLib.VetID = "bob";'
type VetID = string | number;
//~ We can invoke 'myLib.checkCat(c)' or 'myLib.checkCat(c, v);'
function checkCat(c: Cat, s?: VetID);
}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>
/*~ This is the module template file for class modules.
*~ You should rename it to index.d.ts and place it in a folder with the same name as the module.
*~ For example, if you were writing a file for "super-greeter", this
*~ file should be 'super-greeter/index.d.ts'
*/
/*~ Note that ES6 modules cannot directly export class objects.
*~ This file should be imported using the CommonJS-style:
*~ import x = require('someLibrary');
*~
*~ Refer to the documentation to understand common
*~ workarounds for this limitation of ES6 modules.
*/
/*~ If this module is a UMD module that exposes a global variable 'myClassLib' when
*~ loaded outside a module loader environment, declare that global here.
*~ Otherwise, delete this declaration.
*/
export as namespace myClassLib;
/*~ This declaration specifies that the class constructor function
*~ is the exported object from the file
*/
export = MyClass;
/*~ Write your module's methods and properties in this class */
declare class MyClass {
constructor(someParam?: string);
someProperty: string[];
myMethod(opts: MyClass.MyClassMethodOptions): number;
}
/*~ If you want to expose types from your module as well, you can
*~ place them in this block.
*/
declare namespace MyClass {
export interface MyClassMethodOptions {
width?: number;
height?: number;
}
}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>
/*~ This is the module template file for function modules.
*~ You should rename it to index.d.ts and place it in a folder with the same name as the module.
*~ For example, if you were writing a file for "super-greeter", this
*~ file should be 'super-greeter/index.d.ts'
*/
/*~ Note that ES6 modules cannot directly export callable functions.
*~ This file should be imported using the CommonJS-style:
*~ import x = require('someLibrary');
*~
*~ Refer to the documentation to understand common
*~ workarounds for this limitation of ES6 modules.
*/
/*~ If this module is a UMD module that exposes a global variable 'myFuncLib' when
*~ loaded outside a module loader environment, declare that global here.
*~ Otherwise, delete this declaration.
*/
export as namespace myFuncLib;
/*~ This declaration specifies that the function
*~ is the exported object from the file
*/
export = MyFunction;
/*~ This example shows how to have multiple overloads for your function */
declare function MyFunction(name: string): MyFunction.NamedReturnType;
declare function MyFunction(length: number): MyFunction.LengthReturnType;
/*~ If you want to expose types from your module as well, you can
*~ place them in this block. Often you will want to describe the
*~ shape of the return type of the function; that type should
*~ be declared in here, as this example shows.
*/
declare namespace MyFunction {
export interface LengthReturnType {
width: number;
height: number;
}
export interface NamedReturnType {
firstName: string;
lastName: string;
}
/*~ If the module also has properties, declare them here. For example,
*~ this declaration says that this code is legal:
*~ import f = require('myFuncLibrary');
*~ console.log(f.defaultName);
*/
export const defaultName: string;
export let defaultLength: number;
}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>
/*~ This is the module plugin template file. You should rename it to index.d.ts
*~ and place it in a folder with the same name as the module.
*~ For example, if you were writing a file for "super-greeter", this
*~ file should be 'super-greeter/index.d.ts'
*/
/*~ On this line, import the module which this module adds to */
import * as m from 'someModule';
/*~ You can also import other modules if needed */
import * as other from 'anotherModule';
/*~ Here, declare the same module as the one you imported above */
declare module 'someModule' {
/*~ Inside, add new function, classes, or variables. You can use
*~ unexported types from the original module if needed. */
export function theNewMethod(x: m.foo): other.bar;
/*~ You can also add new properties to existing interfaces from
*~ the original module by writing interface augmentations */
export interface SomeModuleOptions {
someModuleSetting?: string;
}
/*~ New types can also be declared and will appear as if they
*~ are in the original module */
export interface MyModulePluginOptions {
size: number;
}
}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>
/*~ This is the module template file. You should rename it to index.d.ts
*~ and place it in a folder with the same name as the module.
*~ For example, if you were writing a file for "super-greeter", this
*~ file should be 'super-greeter/index.d.ts'
*/
/*~ If this module is a UMD module that exposes a global variable 'myLib' when
*~ loaded outside a module loader environment, declare that global here.
*~ Otherwise, delete this declaration.
*/
export as namespace myLib;
/*~ If this module has methods, declare them as functions like so.
*/
export function myMethod(a: string): string;
export function myOtherMethod(a: number): number;
/*~ You can declare types that are available via importing the module */
export interface someType {
name: string;
length: number;
extras?: string[];
}
/*~ You can declare properties of the module using const, let, or var */
export const myField: number;
/*~ If there are types, properties, or methods inside dotted names
*~ of the module, declare them inside a 'namespace'.
*/
export namespace subProp {
/*~ For example, given this definition, someone could write:
*~ import { subProp } from 'yourModule';
*~ subProp.foo();
*~ or
*~ import * as yourMod from 'yourModule';
*~ yourMod.subProp.foo();
*/
export function foo(): void;
}
对应编写好的声明文件,可以发布到npm上,发布时有两种方式:
如果包有一个.js文件,还需要在package.json里指定主声明文件,设置types属性指向捆绑在一起的声明文件,typings和types具有相同的意义,也可以使用。如果声明文件名为index.d.ts并位置在包的根目录里,就不需要使用types属性指定了。
npm会管理所有的依赖,在package.json的dependencies指明了即可。不能再声明文件中使用/// ,应该使用/// 代替。
如果类型依赖于另一个包,不要把依赖包放进自己的包中,保持他们在各自的文件里;不要将声明拷贝到你的包里;如果依赖包没有包含它自己的声明的话,应该依赖于npm类型声明包。
发布到npm库之后,可以直接使用npm install xx进行安装,下载安装之后就可以在代码中引入使用了。多数情况下,类型声明包的名字总是和他们在npm上的包的名字相同,但是有@types/前缀。