Gowhich

Durban's Blog

本次分享如何使用React连接TypeScript开发应用。 到最后,我们会得到一个如下的知识掌握

  • 一个使用React和TypeScript的项目
  • 使用TSLint进行linting
  • JestEnzyme测试
  • 使用Redux进行状态管理

我们将使用create-react-app工具快速初始化一个React项目。

我们假设您已经在使用npm的Node.js。 您可能还想了解React的基础知识。这里就暂不做介绍了,前面的文章有对应的介绍,可以翻一番我前面的分享

安装create-react-app

我们将使用create-react-app,因为它为React项目设置了一些有用的工具和规范默认值。 这只是一个命令行实用程序来构建新的React项目。

1
npm install -g create-react-app

初始化创建项目

我们将创建一个名为ts-react-app的新项目:

1
create-react-app ts-react-app --scripts-version=react-scripts-ts

react-scripts-ts是一组调整,用于采用标准的create-react-app项目管道并将TypeScript引入混合。

此时,您的项目布局应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
├── README.md
├── images.d.ts
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ └── registerServiceWorker.ts
├── tsconfig.json
├── tsconfig.prod.json
├── tsconfig.test.json
└── tslint.json

注意点

  • tsconfig.json包含我们项目的特定于TypeScript的选项。

    • 我们还有一个tsconfig.prod.json和一个tsconfig.test.json,以防我们想要对我们的生产版本或我们的测试版本进行任何调整。
  • tslint.json存储我们的linter,TSLint将使用的设置。

  • package.json包含我们的依赖项,以及我们想要运行的命令的一些快捷方式,用于测试,预览和部署我们的应用程序。

  • public包含静态资产,例如我们计划部署到的HTML页面或图像。 除index.html之外,您可以删除此文件夹中的任何文件。

  • src包含我们的TypeScript和CSS代码。 index.tsx是我们文件的入口点,是必需的。

  • images.d.ts将告诉TypeScript可以导入某些类型的图像文件,create-react-app支持这些文件。

设置源代码管理

我们的测试工具Jest期望存在某种形式的源代码控制(例如Git或Mercurial)。 为了正确运行,我们需要初始化一个git存储库。

1
2
3
4
cd ts-react-app
git init
git add .
git commit -m "Initial commit."

重写默认值

react-scripts-ts设置的TSLint配置有点过于热心。 让我们解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
{
- "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
+ "extends": [],
+ "defaultSeverity": "warning",
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts"
]
}
}

配置TSLint超出了此篇文章要分享的范围,但您可以随意尝试适合您的操作。

运行项目

运行项目就像运行一样简单

1
npm run start

这将运行我们的package.json中指定的启动脚本,并将在我们保存文件时生成重新加载页面的服务器。 通常,服务器在http://localhost:3000运行,但应自动为您打开。 这允许我们快速预览更改,从而收紧迭代循环。

测试项目

测试也只是一个命令:

1
npm run test

此命令针对扩展名以.test.ts或.spec.ts结尾的所有文件运行Jest,这是一个非常有用的测试实用程序。 与npm run start命令一样,Jest会在检测到更改后立即自动运行。 如果您愿意,可以并排运行npm run start和npm run test,以便您可以预览更改并同时测试它们。

创建生产构建

使用npm run start运行项目时,我们最终没有使用优化的构建。 通常,我们希望我们发送给用户的代码尽可能快速和小巧、缩小等某些优化可以实现这一目标,但通常需要更多时间。 我们称之为”production”构建的构建(与开发构建相对)。

要运行生产构建,只需运行即可

1
npm run build

这将分别在./build/static/js和./build/static/css中创建优化的JS和CSS构建。

您不需要在大多数时间运行生产构建,但如果您需要测量应用程序的最终大小等内容,则非常有用。

未完待续…

使用

在TypeScript 2.0中,在获取,使用和查找声明文件时,它变得非常容易。 下面说下如何做到这三点。

下载

在TypeScript 2.0及更高版本中获取类型声明不需要除npm之外的任何工具。

例如,获取像lodash这样的库的声明只需要以下命令

1
npm install --save @types/lodash

值得注意的是,如果npm包已经包含了发布中描述的声明文件,则不需要下载相应的@types包。

使用

从那里你可以在你的TypeScript代码中使用lodash而不用大惊小怪。这适用于模块和全局代码。

例如,一旦你安装了你的类型声明,就可以使用import和write

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

或者如果您不使用模块,则可以使用全局变量_。

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

搜索

在大多数情况下,类型声明包应始终与npm上的包名称相同,但前缀为@types/,但如果需要,可以查看https://aka.ms/types 以查找你最喜欢的库的包。

注意:如果您要搜索的声明文件不存在,您可以随时贡献一份,并帮助下一位寻找它的开发人员。 有关详细信息,请参阅DefinitelyTyped贡献指南页面。

发布

经过前面的文章介绍声明文件的使用,现在您应该可以创作一个声明文件了,并且可以将创作的文件发布到npm了。发布的话可以通过两种主要方式将声明文件发布到npm:

  1. 打包你的npm包
  2. 在npm上发布到@types组织

如果你的包是用TypeScript编写的,那么第一种方法是受欢迎的。 使用–declaration标志生成声明文件。 这样,您的声明和JavaScript始终保持同步。

如果您的包不是用TypeScript编写的,那么第二种方法是首选方法。

在你的npm包中包含声明

如果你的包有一个main.js文件,你还需要在package.json文件中指明主声明文件。 将types属性设置为指向打包的声明文件。 例如:

1
2
3
4
5
6
7
{
"name": "awesome",
"author": "Vandelay Industries",
"version": "1.0.0",
"main": "./lib/main.js",
"types": "./lib/main.d.ts"
}

请注意,“typings”字段与“types”同义,也可以使用。

另请注意,如果主声明文件名为index.d.ts并且位于包的根目录(index.js旁边),则不需要标记“types”属性,但建议这样做。

依赖

所有依赖项都由npm管理。 确保您所依赖的所有声明包都在package.json的“dependencies”部分中进行了适当标记。 例如,假设我们编写了一个使用Browserify和TypeScript的包。

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "browserify-typescript-extension",
"author": "Vandelay Industries",
"version": "1.0.0",
"main": "./lib/main.js",
"types": "./lib/main.d.ts",
"dependencies": {
"browserify": "latest",
"@types/browserify": "latest",
"typescript": "next"
}
}

在这里,我们的包依赖于browserify和typescript包。 browserify不会将其声明文件与其npm包捆绑在一起,因此我们需要依赖@types/browserify来声明它的声明。 另一方面,typescript打包其声明文件,因此不需要任何其他依赖项。

我们的包公开了每个声明的声明,因此我们的browserify-typescript-extension包的任何用户也需要具有这些依赖关系。 出于这个原因,我们使用“依赖”而不是“devDependencies”,因为否则我们的消费者需要手动安装这些包。 如果我们刚刚编写了一个命令行应用程序并且不希望我们的包被用作库,那么我们可能已经使用了devDependencies。

警告

/// <reference path="..." />

不要在声明文件中使用/// <reference path =“...”/>

1
2
/// <reference path="../typescript/lib/typescriptServices.d.ts" />
....

使用/// <reference types="..." />替换

1
2
/// <reference types="typescript" />
....

包装依赖声明

如果您的类型定义依赖于另一个包:

  • 不要将它与你的相结合,将每个文件保存在自己的文件中。
  • 不要复制包中的声明。
  • 如果它没有打包其声明文件,请依赖于npm类型声明包。

发布到@types

@types组织下的软件包将使用types-publisher工具从DefinitelyTyped自动发布。要将您的声明作为@types包发布,请向 https://github.com/DefinitelyTyped/DefinitelyTyped 提交拉取请求。您可以在贡献指南页面中找到更多详细信息。

模板

综合前面的分享,这里分享下官方的模板,方便后面实战中随机查看

global-modifying-module.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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 { };

global-plugin.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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;
}
}

global.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 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);
}

module-class.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 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-function.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 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;
}

module-plugin.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 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;
}
}

module.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 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;
}

深入

定义文件原理:深入

构建模块以提供您想要的精确API形状可能会非常棘手。例如,我们可能想要一个可以使用或不使用new调用的模块来生成不同的类型,在层次结构中公开各种命名类型,并且在模块对象上也有一些属性。

通过深入理解定义文件原理,您将拥有编写复杂定义文件的工具,这些文件可以显示友好的API表面。本指南重点介绍模块(或UMD)库,因为此处的选项更加多样化。

关键概念

通过了解TypeScript如何工作的一些关键概念,您可以完全理解如何进行任何形式的定义。

类型

为了更明确,引入了一种类型:

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

每个声明表单都会创建一个新的类型名称。

与类型一样,您可能已经了解了什么是值。值是我们可以在表达式中引用的运行时名称。例如,让x = 5;创建一个名为x的值。

同样,明确地,以下事物创造价值:

  • let,const和var声明
  • 包含值的命名空间或模块声明
  • 枚举声明
  • 一个类声明
  • 引用值的导入声明
  • 功能声明

命令空间

类型可以存在于名称空间中。例如,如果我们有声明let x: A.B.C,我们说类型C来自A.B命名空间。

这种区别是微妙而重要的 - 在这里,A.B不一定是一种类型或价值。

简单组合:一个名称,多个含义

给定名称A,我们可能会为A找到最多三种不同的含义:类型,值或命名空间。如何解释名称取决于使用它的上下文。例如,在声明中,让m:A.A = A;,A首先用作命名空间,然后用作类型名称,然后用作值。这些含义最终可能指的是完全不同的声明!

这可能看起来令人困惑,但只要我们不过度超载事物,它实际上非常方便。让我们看看这种组合行为的一些有用方面。

内置组合

精明的读者会注意到,例如,类出现在类型和值列表中。声明类C {}创建两件事:一个C类引用类的实例形状,一个值C引用类的构造函数。枚举声明的行为类似。

用户组合

假设我们写了一个模块文件foo.d.ts:

1
2
3
4
export var SomeVar: { a: SomeType };
export interface SomeType {
count: number;
}

然后调用它:

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

这种方法效果很好,但我们可以想象SomeType和SomeVar非常密切相关,因此您希望它们具有相同的名称。我们可以使用组合来在同一个名称Bar下呈现这两个不同的对象(值和类型):

1
2
3
4
export var Bar: { a: Bar };
export interface Bar {
count: number;
}

这为使用代码中的解构提供了一个非常好的机会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Bar } from './foo';  
let x: Bar = Bar.a;
console.log(x.count);
````

同样,我们在这里使用Bar作为类型和值。请注意,我们不必将Bar值声明为Bar类型 - 它们是独立的。

**高级组合**

某些类型的声明可以跨多个声明组合。例如,类C {}和接口C {}可以共存,并且都为C类型提供属性。

只要它不会产生冲突,这是合法的。一般的经验法则是值总是与同名的其他值冲突,除非它们被声明为名称空间,如果使用类型别名声明(type s = string)声明类型,则类型将发生冲突,并且名称空间永远不会发生冲突。

让我们看看如何使用它。

*使用interface添加*

我们可以使用另一个接口声明向接口添加其他成员:

```ts
interface Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

这也适用于类:

1
2
3
4
5
6
7
8
9
class Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

请注意,我们无法使用接口添加类型别名(type s = string;)。

使用namespace添加

命名空间声明可用于以任何不会产生冲突的方式添加新类型,值和命名空间。

例如,我们可以向类添加静态成员:

1
2
3
4
5
6
7
class C {
}
// ... elsewhere ...
namespace C {
export let x: number;
}
let y = C.x; // OK

请注意,在此示例中,我们向C的静态端(其构造函数)添加了一个值。这是因为我们添加了一个值,所有值的容器都是另一个值(类型由名称空间包含,名称空间包含在其他名称空间中)。

我们还可以为类添加命名空间类型:

1
2
3
4
5
6
7
class C {
}
// ... elsewhere ...
namespace C {
export interface D { }
}
let y: C.D; // OK

在这个例子中,在我们为它编写名称空间声明之前,没有名称空间C.C作为命名空间的含义与类创建的C的值或类型含义不冲突。

最后,我们可以使用命名空间声明执行许多不同的合并。这不是一个特别现实的例子,但显示了各种有趣的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
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(因为名称空间声明包含值Z)
  • 命名空间X(因为命名空间声明包含一个类型,Y)
  • X命名空间中的类型Y.
  • X命名空间中的类型Z(类的实例形状)
  • 值Z,它是X值的属性(类的构造函数)

第二个块创建以下名称含义:

  • 值Y(类型编号),它是X值的属性
  • 命名空间Z.
  • 值Z,它是X值的属性
  • X.Z命名空间中的类型C.
  • 值C,它是X.Z值的属性
  • X型

使用export =或import

一个重要的规则是export和import声明export或import其目标的所有含义。

该做什么和不该做什么

一般类型

数字,字符串,布尔值和对象

不要使用Number,String,Boolean或Object类型。 这些类型指的是在JavaScript代码中几乎从不正确使用的非原始盒装对象。

1
2
/* WRONG */
function reverse(s: String): String;

请使用类型number,string和boolean。

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

而不是Object,使用非基本对象类型(在TypeScript 2.2中添加)。

泛型

不要使用不使用其类型参数的泛型类型。在TypeScript FAQ页面中查看更多详细信息。

回调类型

返回回调类型

不要将返回类型any用于其值将被忽略的回调:

1
2
3
4
/* WRONG */
function fn(x: () => any) {
x();
}

对于其值将被忽略的回调,请使用返回类型void:

1
2
3
4
/* OK */
function fn(x: () => void) {
x();
}

原因:使用void更安全,因为它可以防止您以未经检查的方式意外使用x的返回值:

1
2
3
4
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'
}

回调中的可选参数

除非你真的是这样说,否则不要在回调中使用可选参数:

1
2
3
4
/* WRONG */
interface Fetcher {
getObject(done: (data: any, elapsedTime?: number) => void): void;
}

这具有非常具体的含义:完成的回调可以使用1个参数调用,也可以使用2个参数调用。作者可能打算说回调可能不关心elapsedTime参数,但是没有必要使参数可选来完成这一点 - 提供一个接受较少参数的回调总是合法的。

写回调参数是非可选的:

1
2
3
4
/* OK */
interface Fetcher {
getObject(done: (data: any, elapsedTime: number) => void): void;
}

重载和回调

不要编写仅在回调函数参数上不同的单独重载:

1
2
3
/* WRONG */
declare function beforeAll(action: () => void, timeout?: number): void;
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;

应该只使用最大参数个数写一个重载:

1
2
/* OK */
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;

原因:忽略参数的回调总是合法的,因此不需要更短的过载。首先提供较短的回调允许传入错误输入的函数,因为它们匹配第一个重载。

函数重载

顺序

不要在更具体的重载之前放置更多的一般重载:

1
2
3
4
5
6
7
/* WRONG */
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?

通过在更具体的签名之后放置更一般的签名来对重载进行排序:

1
2
3
4
5
6
7
/* 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在解析函数调用时选择第一个匹配的重载。当较早的重载比较晚的重载“更一般”时,后一个重载是有效隐藏的,不能被调用。

使用可选参数

不要写几个仅在尾随参数上有所不同的重载:

1
2
3
4
5
6
/* WRONG */
interface Example {
diff(one: string): number;
diff(one: string, two: string): number;
diff(one: string, two: string, three: boolean): number;
}

尽可能使用可选参数:

1
2
3
4
/* OK */
interface Example {
diff(one: string, two?: string, three?: boolean): number;
}

请注意,只有当所有重载具有相同的返回类型时,才会发生此折叠。

原因:这有两个重要原因。

TypeScript通过查看是否可以使用源的参数调用目标的任何签名来解析签名兼容性,并允许使用无关的参数。例如,只有在使用可选参数正确编写签名时,此代码才会公开错误:

1
2
3
4
5
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的“严格空检查”功能。由于未指定的参数在JavaScript中显示为未定义,因此将显式的undefined传递给带有可选参数的函数通常很好。例如,在严格的空值下,此代码应该是正常的:

使用联合类型

不要只在一个参数位置写入因类型不同的重载:

1
2
3
4
5
6
/* WRONG */
interface Moment {
utcOffset(): number;
utcOffset(b: number): Moment;
utcOffset(b: string): Moment;
}

尽可能使用联合类型:

1
2
3
4
5
/* OK */
interface Moment {
utcOffset(): number;
utcOffset(b: number|string): Moment;
}

请注意,我们在这里没有使b可选,因为签名的返回类型不同。

原因:这对于将“值”传递给函数的人来说非常重要:

1
2
3
4
5
6
7
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);
}

继续上篇文章【如何创建高质量的TypeScript声明文件(五) - 示例】 上篇文章介绍了

  • 全局变量
  • 全局函数
  • 具有属性的对象
  • 重载函数
  • 可重用类型(接口)

几种示例

下面继续分享剩余的几种示例

  • 可重用类型(类型别名)
  • 组织类型

可重用类型(类型别名)

文档

在需要问候语的任何地方,您可以提供字符串,返回字符串的函数或Greeter实例。

代码

1
2
3
4
5
6
7
8
function getGreeting() {
return "howdy";
}
class MyGreeter extends Greeter { }

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

声明

您可以使用类型别名来为类型创建简写:

1
2
3
type GreetingLike = string | (() => string) | MyGreeter;

declare function greet(g: GreetingLike): void;

组织类型

文档

greeter对象可以记录到文件或显示警报。 您可以向.log(…)提供LogOptions,并为.alert(…)提供警报选项

代码

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

声明

使用命名空间来组织类型。

1
2
3
4
5
6
7
8
9
10
declare namespace GreetingLib {
interface LogOptions {
verbose?: boolean;
}
interface AlertOptions {
modal: boolean;
title?: string;
color?: string;
}
}

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

1
2
3
4
5
6
7
8
9
10
11
declare namespace GreetingLib.Options {
// Refer to via GreetingLib.Options.Log
interface Log {
verbose?: boolean;
}
interface Alert {
modal: boolean;
title?: string;
color?: string;
}
}

具有属性的对象

文档

您可以通过实例化Greeter对象来创建一个greeter,或者通过从中扩展来创建一个自定义的greeter。

代码

1
2
3
4
5
6
7
8
9
const myGreeter = new Greeter("hello, world");
myGreeter.greeting = "howdy";
myGreeter.showGreeting();

class SpecialGreeter extends Greeter {
constructor() {
super("Very special greetings");
}
}

声明

使用declare类来描述类或类类对象。 类可以具有属性和方法以及构造函数。

1
2
3
4
5
6
declare class Greeter {
constructor(greeting: string);

greeting: string;
showGreeting(): void;
}

前面四篇文章一起介绍了在声明文件中关于库结构的一些介绍,本篇文章之后分享一些API的文档,还有它们的使用示例,并且阐述如何为他们创建声明文件

这些示例以大致递增的复杂度顺序排序。

  • 全局变量
  • 全局函数
  • 具有属性的对象
  • 重载函数
  • 可重用类型(接口)
  • 可重用类型(类型别名)
  • 组织类型

示例

全局变量

文档

全局变量foo包含存在的小部件数。

代码

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

声明

使用declare var来声明变量。如果变量是只读的,则可以使用declare const。如果变量是块作用域的,您也可以使用declare let。

1
2
/** The number of widgets present */
declare var foo: number;

全局函数

文档

您可以使用字符串调用函数greet来向用户显示问候语。

代码

1
greet("hello, world");

声明

使用declare function声明函数。

1
declare function greet(greeting: string): void;

具有属性的对象

文档

全局变量myLib有一个用于创建问候语的makeGreeting函数,以及一个属性numberOfGreetings,用于指示到目前为止所做的问候数。

代码

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

let count = myLib.numberOfGreetings;

声明

使用declare namespace描述由点式表示法访问的类型或值。

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

重载函数

文档

getWidget函数接受一个数字并返回一个Widget,或者接受一个字符串并返回一个Widget数组。

代码

1
2
3
let x: Widget = getWidget(43);

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

声明

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

可重用类型(接口)

文档

指定问候语时,必须传递GreetingSettings对象。该对象具有以下属性:

1 - 问候语:必填字符串 2 - 持续时间:可选的时间长度(以毫秒为单位) 3 - 颜色:可选字符串,例如”#FF00FF”

代码

1
2
3
4
greet({
greeting: "hello world",
duration: 4000
});

声明

使用接口定义具有属性的类型。

1
2
3
4
5
6
7
interface GreetingSettings {
greeting: string;
duration?: number;
color?: string;
}

declare function greet(setting: GreetingSettings): void;

未完待续…

继续上篇文章[如何创建高质量的TypeScript声明文件(三)]

对UMD库的依赖性

来自全局库

如果您的全局库依赖于UMD模块,请使用/// <reference types指令

1
2
3
/// <reference types="moment" />

function getThing(): moment;

来自模块或UMD库

如果您的模块或UMD库依赖于UMD库,请使用import语句:

1
import * as someLib from 'someLib';

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

补充说明

防止名称冲突

请注意,在编写全局声明文件时,可以在全局范围中定义许多类型。 我们强烈反对这一点,因为当许多声明文件在项目中时,它会导致可能无法解析的名称冲突。

一个简单的规则是仅通过库定义的任何全局变量声明命名空间类型。 例如,如果库定义全局值’cats’,您应该写

1
2
3
declare namespace cats {
interface KittySettings { }
}

而不是

1
2
// at top-level
interface CatsKittySettings { }

还可以确保在不破坏声明文件用户的情况下将库转换为UMD。

ES6对模块插件的影响

某些插件在现有模块上添加或修改顶级导出。 虽然这在CommonJS和其他加载器中是合法的,但ES6模块被认为是不可变的,并且这种模式是不可能的。 因为TypeScript与加载程序无关,所以没有编译时强制执行此策略,但是打算转换到ES6模块加载程序的开发人员应该知道这一点。

ES6对模块呼叫签名的影响

许多流行的库(如Express)在导入时将自身暴露为可调用函数。 例如,典型的Express用法如下所示:

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

在ES6模块加载器中,顶级对象(此处导入为exp)只能具有属性; 顶级模块对象永远不可调用。 这里最常见的解决方案是为可调用/可构造对象定义默认导出; 某些模块加载程序填充程序将自动检测此情况并使用默认导出替换顶级对象。

继续上篇文章[如何创建高质量的TypeScript声明文件(二)]

模块插件或UMD插件

模块插件更改另一个模块(UMD或模块)的形状。 例如,在Moment.js中,时刻范围为时刻对象添加了一个新的范围方法。

出于编写声明文件的目的,无论要更改的模块是普通模块还是UMD模块,您都将编写相同的代码。

模板

使用module-plugin.d.ts模板。

全局插件

全局插件是改变某些全局形状的全局代码。 与全局修改模块一样,这些会增加运行时冲突的可能性。

例如,某些库将新函数添加到Array.prototype或String.prototype。

识别全局插件

全局插件通常很容易从他们的文档中识别出来。

您将看到如下所示的示例:

1
2
3
4
5
6
7
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模板。

全局修改模块

全局修改模块在导入时会更改全局范围中的现有值。 例如,可能存在一个库,在导入时会向String.prototype添加新成员。 由于运行时冲突的可能性,这种模式有点危险,但我们仍然可以为它编写声明文件。

识别全局修改模块

全局修改模块通常很容易从其文档中识别。 通常,它们与全局插件类似,但需要一个require调用来激活它们的效果。

你可能会看到这样的文档:

1
2
3
4
5
6
7
8
9
10
11
12
// '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模板。

使用依赖性

您可能拥有多种依赖关系。

对全局库的依赖

如果您的库依赖于全局库,请使用/// <reference types ="..."/>指令:

1
2
3
/// <reference types="someLib" />

function getThing(): someLib.thing;

对模块的依赖性

如果您的库依赖于模块,请使用import语句:

1
2
3
import * as moment from "moment";

function getThing(): moment;

未完待续…

0%