在我们实际的开发中,可能之前并没有用到 TS,但是后面我们想给项目加入 TS,难道我们要把整个项目重写吗?答案是不用。我们可以通过给项目加上类型声明文件来达到不重写项目还能使用 TS 的目的。

在 TS 中 declare 表示声明的意思,我们可以用它来实现各种声明。类型声明在编译的时候都会被删除,不会影响真正的代码。下面列举了几种常用的类型声明:

  1. declare var 声明全局变量
  2. declare function 声明全局方法
  3. declare class 声明全局类
  4. declare enum 声明全局枚举类型
  5. declare namespace 声明(含有子属性的)全局对象
  6. interface type 声明全局类型

普通类型声明

我们可以使用 declare 声明一个变量:

  1. declare let name: string; //变量
  2. console.log(name)

编译后的结果:

  1. (function () {
  2. 'use strict';
  3. console.log(name);
  4. })();

可以看到,虽然我们在 index.ts中给声明了变量 name ,但是编译后的结果并没有包含 declare 声明的变量,直接使用了一个未声明的变量的 name

外部枚举

外部枚举是使用declare enum定义的枚举类型,外部枚举用来描述已经存在的枚举类型的形状:

  1. declare enum Direction {
  2. UP,
  3. DOWN,
  4. LEFT,
  5. RIGHT
  6. }
  7. let direction = [
  8. Direction.UP,
  9. Direction.DOWN,
  10. Direction.LEFT,
  11. Direction.RIGHT
  12. ];

编译后的结果如下:

  1. (function () {
  2. 'use strict';
  3. [
  4. Direction.UP,
  5. Direction.DOWN,
  6. Direction.LEFT,
  7. Direction.RIGHT
  8. ];
  9. })();

下面我们看下常量枚举:

  1. declare const enum Direction {
  2. UP,
  3. DOWN,
  4. LEFT,
  5. RIGHT
  6. }
  7. let direction = [
  8. Direction.UP,
  9. Direction.DOWN,
  10. Direction.LEFT,
  11. Direction.RIGHT
  12. ];

结果如下:

  1. (function () {
  2. 'use strict';
  3. var direction = [
  4. 0 /* UP */,
  5. 1 /* DOWN */,
  6. 2 /* LEFT */,
  7. 3 /* RIGHT */
  8. ];
  9. console.log(direction);
  10. })();

namespace

如果一个全局变量包含了很多属性,这时我们可以使用namespace。注意:在命名空间内部的变量不需要使用declare声明:

  1. declare namespace Vue {
  2. const version: string;
  3. function nextTick(cb?: () => void):Promise<void>
  4. }
  5. Vue.version;
  6. Vue.nextTick(() => { })

编译结果:

  1. (function () {
  2. 'use strict';
  3. Vue.version;
  4. Vue.nextTick(function () { });
  5. })();

类型声明文件

在实际开发中我们会把类型声明放到一个单独的文件中,这个文件的命名遵循 *.d.ts ,在使用第三方库时,阅读类型声明文件可以帮助我们了解库的使用方式。

下面我们把使用 namespace 定义的 Vue, 抽离到单独的文件中:

  1. declare namespace Vue {
  2. const version: string;
  3. function nextTick(cb?: () => void):Promise<void>
  4. }
  1. Vue.version;
  2. Vue.nextTick(() => { })
  3. export { };

配置 tsconfig.json 文件:

  1. {
  2. // ...
  3. "include": [
  4. "src/**/*",
  5. "typings/**/*"
  6. ]
  7. }

这里简单说下配置文件中的 include 选项,它主要用来指定需要编译处理的文件列表,支持 glob 模式匹配,文件的解析路径相对于当前项目的tsconfig.json文件位置,详细请阅读 官网

但是一般我们在使用第三方库的时候不可能所有类型声明文件都要我们手写,我们可以安装第三方类型声明文件,所有的第三方的类型声明库都会带有@types 前缀,这是约定。JS 本身有很多内置对象,我们把它们被当做声明好的类型使用。 我们可以在 这里 查看内置对象的类型声明文件。

我们以使用 jQuery 举例:

安装 jQuery:

  1. cnpm i jquery -D

src/index.ts 文件中导入:

  1. import * as jQuery from 'jquery';
  2. jQuery.ajax();

上述代码报错 Cannot find module 'jquery'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?我们需要在 tsconfig.json文件中配置 moduleResolution选项:

  1. {
  2. "compilerOptions": {
  3. "moduleResolution": "node"
  4. }
  5. }

配置完成后代码还是会报错:

  1. Could not find a declaration file for module 'jquery'. '/Users/sun/Desktop/learn/ts/ts/node_modules/jquery/dist/jquery.js' implicitly has an 'any' type.
  2. Try `npm i --save-dev @types/jquery` if it exists or add a new declaration (.d.ts) file containing `declare module 'jquery';`

我们需要在终端执行 npm i --save-dev @types/jquery ,执行完安装命令后可以看到代码正常运行了。我们可以在 node_modules/@types 文件下看到多了一个 jquery 文件,打开文件可以看到里面放的几乎全是类型声明文件。这里我们借助这个例子来简要说下类型声明文件的查找顺序:

  1. 先查找 node_modules/jquery/package.json 文件,看看里面的 types 属性有没有指定声明文件地址,如:"types":"types/xxx.d.ts"
  2. 没有找到的话会去查找 node_modules/jquery/index.d.ts 文件
  3. 如果还没找到,会去查找 node_modules/@types/jquery/index.d.ts 文件
  4. 最后查找 typings\jquery\index.d.ts
  5. 如果都没找到就会报错。

细心的朋友的可能看到了,在上述代码中我们导入 jQuery 用的是 import * as jQuery from 'jquery'; 为啥要这么用呢,直接 import jQuery from 'jquery'; 不行吗?试过之后发现不行,报错了:

  1. Module '"/Users/sun/Desktop/learn/ts/ts/node_modules/@types/jquery/index"' can only be default-imported using the 'allowSyntheticDefaultImports' flag

查看下 jQuery 的类型声明文件,发现它是像下面这样导出的:

  1. // 这是 TS 类型声明的一种导出语法
  2. export = jQuery;

查阅官方文档 可以看到有下面这句话:

  1. Note that using export default in your .d.ts files requires esModuleInterop: true to work. If you cant have esModuleInterop: true in your project, such as when youre submitting a PR to Definitely Typed, youll have to use the export= syntax instead. This older syntax is harder to use but works everywhere. Heres how the above example would have to be written using export=:

我们可以在 tsconfig.json文件中配置 esModuleInterop: true

  1. {
  2. "compilerOptions": {
  3. // 省略代码
  4. "esModuleInterop": true
  5. // 省略代码
  6. }
  7. }

然后修改 node_modules/@types/jquery/index.d.ts 导出形式为 export default jQuery;再修改 src/index.ts中的导入为 import jQuery from 'jquery';

  1. import jQuery from 'jquery';
  2. console.log(jQuery)
  3. jQuery.ajax('');
  4. export { };

可见代码运行正常。但是上面我们修改了 jQuery 库的类型声明文件导出方式,这显示是不合适的,在开发中我们一般很少修改库的源代码。实际上我们配置了 esModuleInterop: true 后直接就可以使用 import jQuery from 'jquery';了,这是因为 esModuleInterop 配置项的主要作用如下:

  1. Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'.

扩展全局变量的类型

我们可以使用 interface 扩展全局变量类型。在下面的例子中,我们给 String扩展了一个全局变量函数 log

  1. declare var String: StringConstructor;
  2. interface StringConstructor {
  3. new(val?: any): String;
  4. (val?: any): String;
  5. readonly prototype: String
  6. }
  7. interface String {
  8. toString(): string
  9. }
  10. interface String {
  11. log(): void
  12. }
  13. String.prototype.log = function () {
  14. console.log(this);
  15. };
  16. 'fang'.log(); // 'fang'
  17. // 注意,这里不能加 export,因为要给全局扩展
  18. // export {}

下面的例子我们给 window 扩展了一个全局变量 fang

  1. interface Window {
  2. fang: number
  3. }
  4. window.fang;
  5. // 不能使用 export ,因为要给全局扩展

细心的读者可能看到了上面两个代码中都明确标明了不能使用 export 也就是不能将文件变为局部模块。如果我们非要使用呢?怎么解决报错问题呢?我们可以使用 declare global 来扩展全局变量类型:

  1. declare var String: StringConstructor;
  2. interface StringConstructor {
  3. new(val?: any): String;
  4. (val?: any): String;
  5. readonly prototype: String
  6. }
  7. declare global {
  8. interface String {
  9. toString():string
  10. }
  11. interface String {
  12. log(): void
  13. }
  14. interface Window {
  15. fang: string;
  16. }
  17. }
  18. String.prototype.log = function () {
  19. console.log(this);
  20. };
  21. 'fang'.log();
  22. window.fang;
  23. export { };

注意,我们不能在一个单独的文件里直接使用上面的方法进行全局扩展,如:

  1. declare global {
  2. interface Window {
  3. age: string;
  4. }
  5. }

这样会报如下错误:

  1. Augmentations for the global scope can only be directly nested in external modules or ambient module declarations.

我们需要使用 export 导出才行:

  1. declare global {
  2. interface Window {
  3. age: string;
  4. }
  5. }
  6. export {}

合并声明

在 TS 中有些关键字声明的变量既可以作为类型使用又可以作为值使用,有些只能作为类型使用,而有些只能作为值使用:

关键字 是否可以作为类型使用 是否可以作为值使用
class
enum
interface
type
function
var、let、const

从上面的表格中我们可以看出类既可以作为类型使用,也可以作为值使用:

  1. class Student {
  2. name: string = '';
  3. }
  4. // 这种情况是作为类型使用的
  5. let s1: Student;
  6. // 这种情况是作为值使用的
  7. let s = new Student;

合并类型声明

同一名称的两个独立声明会被合并成一个,合并后的声明拥有原先两个声明的特性。比如接口,接口只能作为类型使用,不能作为值使用,下面的代码虽然声明了两个接口,但它们会进行合并成一个,所以给 p 赋值时,需要包含两个属性:

  1. interface Person {
  2. name: string;
  3. }
  4. interface Person {
  5. age: number;
  6. }
  7. const p: Person = {
  8. name: 'f',
  9. age: 18
  10. };

我们可以利用接口合并的特性给第三方扩展类型声明:

  1. interface Person {
  2. name: string;
  3. }
  1. interface Person{
  2. age: number;
  3. }
  4. const p: Person = {
  5. name: 'f',
  6. age: 18
  7. };

使用命名空间进行一些扩展

我们还可以借助 namespace 实现很多功能。下面我们来简单列举下在开发过程中我们可能用到的几种情况:

  1. 我们可以使用命名空间扩展类

    1. class Student {
    2. name!: string;
    3. age!: number;
    4. scores!: Student.Scores;
    5. }
    6. namespace Student {
    7. export class Scores {
    8. Math: number = 100;
    9. English: number = 100;
    10. Chinese: number = 100;
    11. }
    12. }
    13. let student: Student = {
    14. name: 'f',
    15. age: 18,
    16. scores: {
    17. Math: 100,
    18. English: 100,
    19. Chinese: 100
    20. }
    21. };

    上面的例子中我们使用命名空间给 Student 类扩展了 scores 属性。

  2. 使用命名空间扩展函数 ```typescript function eat(name: string): string { return eat.person + name; }

namespace eat { export let person = “fang”; }

console.log(eat(‘apple’)) // ‘fangapple’

  1. 3. 使用命名空间扩展枚举类型
  2. ```typescript
  3. enum Color {
  4. red = 1
  5. }
  6. namespace Color {
  7. export const green=2;
  8. }
  9. console.log(Color.green) // 2

下面我们来道题练练手:

  1. import { createStore, Store } from 'redux';
  2. const store = createStore(state=>state);
  3. // any Property 'name' does not exist on type 'Store<unknown, Action<any>>'.
  4. store.name = 'fang';

上面代码中我们想给 store 添加一个 name 属性,但是代码报错了,怎么样修改才可以正常运行呢?仔细想想哈,先不要看下面的答案。

你想出来了吗,下面我们来揭晓答案:

  1. import { createStore, Store } from 'redux';
  2. type StoreName = Store & { name: string };
  3. const store:StoreName = createStore(state=>state);
  4. store.name = 'fang';

生成声明文件

当我们把 TS 编译成 JS 后,会丢失类型声明:

  1. let num: number = 1;
  2. console.log(num);

编译后的结果:

  1. var num = 1;
  2. console.log(num);

可以看到类型声明没有了,这样如果我们想让别人使用咱们打包后的文件时,咱们写的类型声明也就失效了。这应该怎么办呢?

我们可以配置 tsconfig.json文件中的 declaration 选项为 true

  1. {
  2. "compilerOptions": {
  3. "declaration": true, /* Generates corresponding '.d.ts' file. */
  4. }
  5. }

然后执行打包命令,这样选项主要用来设置在编译时是否生成对应的 .d.ts 文件:

类型声明实战

前面我们讲了那么多类型声明相关的东西,不能光说不练嘴把式吧。下面我们来个简单的例子来练练手,给下面代码添加类型声明:

  1. import { MyEventEmitter } from 'my-events';
  2. const e = new MyEventEmitter;
  3. e.on('click', function () { })
  4. e.emit('click');
  5. e.once('click', function () { });
  6. e.addListener('click', function () { })
  7. export { };

在前面讲过的 types 文件下新建 my-events 文件夹,再在这个文件下新建 index.d.ts 文件,将下面的代码粘贴过去即可。

  1. type Type = string | symbol;
  2. type Listener = (...args: any[]) => void;
  3. export class MyEventEmitter {
  4. on(type: Type, listener: Listener): this;
  5. emit(type: Type, ...args: any[]): boolean;
  6. once(type: Type, listener: Listener): this;
  7. addListener(type: Type, listener: Listener):this;
  8. }