「TypeScript」关于声明

898 阅读23分钟

前言

在对 TypeScript 进行了解过后,这篇文章针对书写高质量的 TypeScript 声明文件。

「TypeScript」 开发手册梳理(上)

「TypeScript」 开发手册梳理(下)

目录结构及重点概括如下:

  • 结构:帮助了解常见库的格式以及如何为每种格式书写正确的声明文件。

如果正在书写新的声明文件,那么可阅读此章节以理解库的不同格式是如何影响声明文件的书写的。

  • 举例:展示了很多常见的API模式以及如何为它们书写声明文件。

我们常通过一些示例来了解第三方库是如何工作的,同时我们需要为这样的库书写声明文件。

  • 规范:指出了 TypeScript 中常见的错误, 描述了如何发现它们, 与怎样去修复。
  • 深入:解释了很多高级书写声明文件的高级概念, 以及展示了如何利用这些概念来创建整洁和直观的声明文件。
  • 模版:帮助你找到一些声明文件以快速开始书写一个新声明文件。(与结构内容融合)
  • 获取、使用和查找声明文件:提供了一些简单步骤来定位与安装相应的声明文件。

目的

  1. 内容简洁且核心
  2. 排版清晰且了然
  3. 语义通顺且自然
  4. 个人思考且说明

结构

你组织声明文件的结构方式取决于库是如何被使用的,针对每种主要库的组织模式,在模版一节都有对应的文件,可利用其快速上手。

识别库的类型

识别库的类型是书写声明文件的第一步,根据库的文档及组织结构不同,这两种方式可能一个会比另外的那个简单一些。

全局库

全局库是指能在全局命名空间下访问的(不需要使用任何形式的import),许多库都是简单的暴露出一个或多个全局变量。

如果 jQuery$变量可以被够简单的引用

并且经常会在全局库的指南文档上看到如何在HTML里用脚本标签引用库

<script src="http://a.great.cdn.for/someLib.js"></script>

大多数流行的全局访问型库实际上都以 UMD 库的形式进行书写(见后文)。

从代码上识别全局库

全局库的源代码时常有以下特点

  • 顶级的声明语句或function声明
  • 一个或多个赋值语句到window.someName
  • 假设DOM原始值像documentwindow是存在的

不会看到:

  • 检查是否使用或如何使用模块加载器,比如requiredefine
  • CommonJS/Node.js 风格的导入如var fs = require("fs");
  • define(...)调用
  • 文档里说明了如何去 require 或导入这个库

一个全局的 Hello, world 库例子可能如下:

function createGreeting(s) {
    return "Hello, " + s;
}
// 或
window.createGreeting = function(s) {
    return "Hello, " + s;
}

声明模版

global.d.ts - 以 myLib 库为例子

// 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);
}

注意要防止命名冲突

模块化库

模块化库只能工作在模块加载器的环境下,ES6,CommonJS 和 RequireJS 具有相似的导入一个模块的表示方法。

对于TypeScript或ES6

import fs = require("fs");

对于JavaScript CommonJS (Node.js)

var fs = require("fs");

你通常会在模块化库的文档里看到如下说明:

var someLib = require('someLib');
// 或
define(..., ['someLib'], function(someLib) {
});

从代码上识别模块化库

标志性特点:

  • 无条件的调用requiredefine
  • import * as a from 'b'; or export c;这样的声明
  • 赋值给exportsmodule.exports

极少包含:

  • windowglobal的赋值

例子

许多流行的Node.js库都是这种模块化的,例如expressgulprequest

声明模版

  • 如果模块能够作为函数调用

module-function.d.ts

// 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;
}
  • 如果模块使用new构造

module-class.d.ts

// 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;
    }
}
  • 如果模块不能被调用或构造。

module.d.ts

// 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;
}

UMD

UMD模块是指那些既可以作为模块使用(通过导入)又可以作为全局(在没有模块加载器的环境里)使用的模块。

在Node.js或RequireJS里,你可以这样写:

import moment = require("moment");
console.log(moment.format());

在浏览器环境里你也可以这样写:

console.log(moment.format());

识别 UMD 库

UMD模块会检查是否存在模块加载器环境。

(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define(["libName"], factory);
    } else if (typeof module === "object" && module.exports) {
        module.exports = factory(require("libName"));
    } else {
        root.returnExports = factory(root.libName);
    }
}(this, function (b) {})

当在库的源码里看到了typeof definetypeof window,或typeof module这样的测试,尤其是在文件的顶端,那么它几乎就是一个UMD库。

UMD库的文档里经常会包含通过require“在Node.js里使用”例子, 和“在浏览器里使用”的例子。

例子

大多数流行的库现在都能够被当成UMD包。 比如 jQuery,Moment.js,lodash和许多其它的。

模块插件或 UMD 插件

一个模块插件可以改变一个模块的结构(UMD或模块)。

例如,在Moment.js里, moment-range添加了新的range方法到monent对象。

模版

模块插件或 UMD 插件的声明文件,会写相同的代码不论被改变的模块是一个纯粹的模块还是UMD模块。

module-plugin.d.ts

// 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;
    }
}

全局插件

一个全局插件是全局代码,它们会改变全局对象的结构,对于全局修改的模块,在运行时存在冲突的可能。

一些库往Array.prototypeString.prototype里添加新的方法。

识别全局插件

全局通常很容易地从它们的文档识别出来(直接在原型上自定义方法或属性)。

var x = "hello, world";
// Creates new methods on built-in types
console.log(x.startsWithHello());

var y = [1, 2, 3];
// Creates new methods on built-in types
console.log(y.reverseAndSort());

模版

global-plugin.d.ts

// 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;
    }
}

全局修改的模块

当一个全局修改的模块被导入的时候,它们会改变全局作用域里的值。这种模式很危险,因为可能造成运行时的冲突。

当导入String.prototype的时候,一些库添加新的成员到其中。

识别全局修改的模块

它们与全局插件相似,但是需要 require调用来激活它们的效果。

// 'require' call that doesn't use its return value
var unused = require("magic-string-time");
/* or */
require("magic-string-time");

var x = "hello, world";
// Creates new methods on built-in types
console.log(x.startsWithHello());

var y = [1, 2, 3];
// Creates new methods on built-in types
console.log(y.reverseAndSort());

模版

global-modifying-module.d.ts

// 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 { };

使用依赖

一般库会存在以下几种依赖。

依赖全局库

依赖于某个全局库时,使用/// <reference types="..." />指令

/// <reference types="someLib" />

function getThing(): someLib.thing;

依赖模块

依赖于模块时,使用import语句:

import * as moment from "moment";

function getThing(): moment;

依赖UMD库

全局库

全局库依赖于某个UMD模块时,使用/// <reference types指令:

/// <reference types="moment" />

function getThing(): moment;

一个模块或UMD库

模块或UMD库依赖于一个UMD库,使用import语句:

import * as someLib from 'someLib';

不要使用/// <reference指令去声明UMD库的依赖!

注意事件

防止命名冲突

在书写全局声明文件时,允许在全局作用域里定义很多类型。

十分不建议这样做,当一个工程里有许多声明文件时,会导致无法处理的命名冲突。

一个简单的规则是使用库定义的全局变量名来声明命名空间类型。

比如,库定义了一个全局的值 cats,你可以利用命名空间声明

declare namespace cats {
    interface KittySettings { }
}

不要直接这样搞

// at top-level
interface CatsKittySettings { }

这样对于声明文件用户来说,同时保证库在转换成UMD的时候没有任何的破坏式改变。

ES6模块插件的影响

一些插件会添加或修改已存在的顶层模块的导出部分。

而这种模式在 CommonJS 和其它加载器里是允许的,在 ES6 模块就不可行,其顶层模块是不被可修改、添加。

同时 TypeScript 不能预知加载器类型,不能在编译时保证其类型,因此开发者在要重构到 ES6 模块加载器上时需注意。

ES6模块调用签名的影响

很多流行库,比如Express,暴露出自己作为可以调用的函数。

比如,典型的Express使用方法如下:

import exp = require("express");
var app = exp();

在 ES6 模块加载器里,顶层的对象(exp)只能具有属性,顶层的模块对象永远不能被调用。

十分常见的解决方法是定义一个 default导出到一个可调用的/可构造的对象,模块加载器助手工具能够自己探测到这种情况并且使用 default导出来替换顶层对象。

举例

展示一些API的文档,还有它们的使用示例, 并且阐述了如何为它们书写声明文件。

例子

全局变量

文档:全局变量foo包含了存在组件总数。

代码

console.log("Half the number of widgets is " + (foo / 2));

声明:使用declare var声明变量,如果变量是只读的,那么可以使用 declare const。 还可以使用 declare let如果变量拥有块级作用域。

/** 组件总数 */
declare var foo: number;

全局函数

文档:用一个字符串参数调用greet函数向用户显示一条欢迎信息。

代码

greet("hello, world");

声明:使用declare function声明函数。

declare function greet(greeting: string): void;

带属性的对象

文档:全局变量myLib包含一个makeGreeting函数, 还有一个属性 numberOfGreetings指示目前为止欢迎数量。

代码

let result = myLib.makeGreeting("hello, world");
console.log("The computed greeting is:" + result);

let count = myLib.numberOfGreetings;

声明:使用declare namespace描述用点表示法访问的类型或值。

declare namespace myLib {
    function makeGreeting(s: string): string;
    let numberOfGreetings: number;
}

函数重载

文档:getWidget函数接收一个数字,返回一个组件,或接收一个字符串并返回一个组件数组。

代码

let x: Widget = getWidget(43);

let arr: Widget[] = getWidget("all of them");

声明:重载

declare function getWidget(n: number): Widget;
declare function getWidget(s: string): Widget[];

可重用类型(接口)

文档:当指定一个欢迎词时,你必须传入一个GreetingSettings对象。 这个对象具有以下几个属性:

1- greeting:必需的字符串

2- duration: 可靠的时长(毫秒表示)

3- color: 可选字符串,比如‘#ff00ff’

代码

greet({
  greeting: "hello world",
  duration: 4000
});

声明:使用interface定义一个带有属性的类型。

interface GreetingSettings {
  greeting: string;
  duration?: number;
  color?: string;
}

declare function greet(setting: GreetingSettings): void;

可重用类型(类型别名)

文档:在任何需要欢迎词的地方,你可以提供一个string,一个返回string的函数或一个Greeter实例。

代码

function getGreeting() {
    return "howdy";
}
class MyGreeter extends Greeter { }

greet("hello");
greet(getGreeting);
greet(new MyGreeter());

声明:你可以使用类型别名来定义类型的短名:

type GreetingLike = string | (() => string) | MyGreeter;

declare function greet(g: GreetingLike): void;

组织类型

文档:greeter示例对象能够记录到文件或显示一个警告,可以为 .log(...)提供LogOptions和为.alert(...)提供选项。

代码

const g = new Greeter("Hello");
g.log({ verbose: true });
g.alert({ modal: false, title: "Current Greeting" });

声明:使用命名空间组织类型。

declare namespace GreetingLib {
    interface LogOptions {
        verbose?: boolean;
    }
    interface AlertOptions {
        modal: boolean;
        title?: string;
        color?: string;
    }
}

也可以在一个声明中创建嵌套的命名空间:

declare namespace GreetingLib.Options {
    // Refer to via GreetingLib.Options.Log
    interface Log {
        verbose?: boolean;
    }
    interface Alert {
        modal: boolean;
        title?: string;
        color?: string;
    }
}

文档:通过实例化Greeter对象来创建欢迎词,或者继承Greeter对象来自定义欢迎词。

代码

// 创建欢迎词
const myGreeter = new Greeter("hello, world");
myGreeter.greeting = "howdy";
myGreeter.showGreeting();

// 继承重构
class SpecialGreeter extends Greeter {
    constructor() {
        super("Very special greetings");
    }
}

声明:使用declare class描述一个类或像类一样的对象,类可以有属性和方法,就和构造函数一样。

declare class Greeter {
    constructor(greeting: string);

    greeting: string;
    showGreeting(): void;
}

规范

普通类型

NumberStringBooleanObject

这些类型指的是非原始的装盒对象,它们几乎没在JavaScript代码里正确地使用过。不要使用如下类型NumberStringBooleanObject

/* 错误 */
function reverse(s: String): String;

应该使用类型numberstring,和 boolean

/* OK */
function reverse(s: string): string;

使用非原始的object类型来代替ObjectTypeScript 2.2新增特性

泛型

不要定义一个从来没使用过其类型参数的泛型类型。

看一个示例:

interface Something<T> {
  name: string;
}
let x: Something<number>;
let y: Something<string>;

// Expected error: Can't convert Something<number> to Something<string>!
x = y;

根本原因是泛型 T 未被使用。

TypeScript 使用结构化类型系统,Something<number>在确定和兼容Something<string>兼容性时,将检查每种类型的每个成员。如果所有成员都兼容,那么类型本身就是兼容的。

但是因为Something<T>没有在任何成员中使用 T,所以类型 T 是什么将推断不出来

泛型与类型是否兼容无关。

// ok
interface Something<T> {
  name: T;
}

回调函数类型

回调函数返回值类型

不要为没有返回值的回调函数设置一个any类型的返回值类型:

/* 错误 */
function fn(x: () => any) {
    x();
}

应该给返回值被忽略的回调函数设置void类型的返回值类型:

/* OK */
function fn(x: () => void) {
    x();
}

使用void相对安全,其防止了你不小心使用x的返回值:

function fn(x: () => void) {
    var k = x(); // oops! meant to do something else
    k.doSomething(); // error, but would be OK if the return type had been 'any'
}

回调函数里的可选参数

非必需不可,不要在回调函数里使用可选参数

/* 错误 */
interface Fetcher {
    getObject(done: (data: any, elapsedTime?: number) => void): void;
}

这里有一种特殊的意义:done回调函数可能以1个参数或2个参数调用。

大概的意思是说这个回调函数不在乎是否有 elapsedTime参数,回调函数可以允许提供一个接收较少参数的回调函数。

/* 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;

回调函数总是可以忽略某个参数的,因此没必要为参数少的情况写重载。

函数重载

顺序

不要把一般的重载放在精确的重载前面:

/* 错误 */
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,将判断为 any

应该排序重载令精确的排在一般的之前:

/* 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, :)

TypeScript会选择第一个匹配到的重载当解析函数调用的时候。 当前面的重载兼容后面的类型,那么后面的被隐藏了不会被调用。

使用可选参数

不要为仅在末尾参数不同时写不同的重载:

/* 错误 */
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;
}

这在所有重载都有相同类型的返回值时会不好用,有以下两个重要原因。

第一个原因是:TypeScript 解析签名兼容性时会查看是否某个目标签名能够使用源的参数调用, 且允许外来参数。 下面的代码暴露出一个bug,当签名被正确的使用可选参数书写时:

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);

第二个原因是:当使用了TypeScript“严格检查null”特性时。 因为没有指定的参数在JavaScript里表示为 undefined,通常显示地为可选参数传入一个undefined。 这段代码在严格null模式下可以工作:

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;
}

注意因为签名的返回值类型不同,则没有让b成为可选的

深入

组织模块以提供你想要的 API 形式保持一致是比较难的,了解如何书写复杂的暴露出友好API的声明文件,这篇指定针对于模块(UMD)库,因为其的选择具有更高的可变性。

比如,可能想要这样一个模块,可以用或不用 new来创建不同的类型, 在不同层级上暴露出不同的命名类型, 且模块对象上还带有一些属性。

核心概念

当理解了一些关于 TypeScript 是如何工作的核心概念, 那么就可能够为任何结构书写声明文件。

类型

TypeScript 里的类型,通过以下方式引入:

  • 类型别名声明(type sn = number | string;
  • 接口声明(interface I { x: number[]; }
  • 类声明(class C { }
  • 枚举声明(enum E { A, B, C }
  • 指向某个类型的import声明

以上每种声明形式都会创建一个新的类型名称,也就是可以被 TypeScript 识别解析。

值是运行时名字,可以在表达式里引用。

比如 let x = 5;创建一个名为x的值。

TypeScript 里的值,通过以下方式能够创建值:

  • letconst,和var声明
  • 包含值的namespacemodule声明
  • enum声明
  • class声明
  • 指向值的import声明
  • function声明

命名空间

类型可以存在于命名空间里,也就是 JavaScript 中的作用域,用来 .

比如,有这样的声明 let x: A.B.C, 我们就认为 C类型来自A.B命名空间。

注意这里,A.B不是必需的类型或值。

简单组合

当一个给定的名字A,可以找出三种不同的意义:一个类型,一个值或一个命名空间,要如何去解析这个名字要看它所在的上下文是怎样的。

比如,在声明 let m: A.A = A;A首先被当做命名空间,然后做为类型名,最后是值。 这些意义最终可能会指向完全不同的声明!

内置组合

class可在 TypeScript 中同时声明在类型

class C { }声明创建了两个东西: 类型C指向类的实例结构, C指向类构造函数。

同时枚举声明拥有相似的行为。

用户组合

假设写了模块文件foo.d.ts:

export var SomeVar: { a: SomeType };
export interface SomeType {
  count: number;
}

使用:

import * as foo from './foo';
let x: foo.SomeType = foo.SomeVar.a;
console.log(x.count);

这样可以很好地工作,但是从逻辑上讲我们知道SomeTypeSomeVar很相关,因此想让他们有相同的名字,可以使用组合通过相同的名字 Bar表示这两种不同的对象(值和对象):

export var Bar: { a: Bar };
export interface Bar {
  count: number;
}

解构使用:

import { Bar } from './foo';
let x: Bar = Bar.a;
console.log(x.count);

这里使用Bar做为类型和值。 注意没有声明 Bar值为Bar类型 -- 它们是独立的。

高级组合

有一些声明能够通过多个声明组合,只要不产生冲突就是合法的。

比如, class C { }interface C { }可以同时存在并且都可以做为C类型的属性。

类型冲突发生在使用类型别名声明的情况下( type s = string), 命名空间永远不会发生冲突。

利用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

注意不能使用接口往类型别名里添加成员(type s = string;

因为同样是在声明类型

使用namespace添加

namespace声明可以用来添加新类型,值和命名空间,只要不出现冲突。

添加静态成员到一个类:

class C {
}
// ... elsewhere ...
namespace C {
  export let x: number;
}
let y = C.x; // OK

注意这个例子里,添加一个值到C静态部分(它的构造函数)。

因为我们添加了一个,且其它值的容器是另一个值 (类型包含于命名空间,命名空间包含于另外的命名空间)。

给类添加一个命名空间类型:

class C {
}
// ... elsewhere ...
namespace C {
  export interface D { }
}
let y: C.D; // OK

这个例子里,直到我们写了namespace声明才有了命名空间C。 做为命名空间的 C不会与类创建的值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(类的构造函数)

第二个代码块创建了以下名字与含义:

  • Ynumber类型),它是值X的一个属性
  • 一个命名空间Z
  • Z,它是值X的一个属性
  • X.Z命名空间下的类型C
  • X.Z的一个属性值C
  • 类型X

使用export =import

一个重要的原则是exportimport声明会导出或导入目标的所有含义

获取、使用和查找声明文件

获取

在 TypeScript 2.0 以上的版本,获取类型声明文件只需要使用 npm。

比如,获取 lodash 库的声明文件,只需使用下面的命令:

npm install --save @types/lodash

如果一个 npm 包里已经包含了它的声明文件,那就不必再去下载相应的@types包了。

使用

获取完后,可以直接在 TypeScript 里使用。 不论是在模块里还是全局代码里使用。

比如,已经npm install安装了类型声明,你可以使用导入:

import * as _ from "lodash";
_.padStart("Hello TypeScript!", 20, " ");

如果你没有使用模块,那么你只需使用全局的变量_

_.padStart("Hello TypeScript!", 20, " ");

查找

大多数情况下,类型声明包的名字总是与它们在npm上的包的名字相同,但是有@types/前缀。

但如果你需要的话,你可以在 aka.ms/types这里查找你喜…