关于类型声明文件 - 01理解

配置项 allowJs

是否编译 .js 文件. 如果你的项目中有自定义的 .js 文件, 并且在 .ts 文件内有引入该 .js 文件, 那么必须设置 allowJs: true, 否则 tsc 编译后不会将该 .js 文件编译到 dist("outDir": "./dist") 目录内, 运行时会造成找不模块错误.

// allowJs:fasle

├─tsconfig.json
├─src
|  ├─index.ts               // 引入了 ./js/index
|  ├─js
|  | ├─index.d.ts
|  | └index.js
├─dist
|  └index.js                // 为编译js文件

// allowJs: true

├─tsconfig.json
├─src
|  ├─index.ts               // 引入了 ./js/index
|  ├─js
|  | ├─index.d.ts
|  | └index.js
├─dist
|  ├─index.js
|  ├─js                         // 编译了js文件
|  | └index.js

其中:

// src/index.ts
import { testJs } from './js/index'  

console.log(testJs("bgg"))
// src/js/index.js
export function testJs(name) {
  name = 'hello' + name
  return name
}

allowJs.d.ts(src/js文件夹内) 的不同组合情况

  • allowJS: fasle(默认值) 并且无 .d.ts 文件时,在 vscode 编辑器中, 会直接用红色波浪线报错, "无法找到模块 ./js/index的声明文件".
  • allowJS: false 并且存在 .d.ts 文件时: 编辑器内不再提示报错, 但因为 allowJs: false, 在运行编译后的文件时仍然报错, 因为 src/js/index.js 文件根本不会编译到 dist 文件夹内.
  • allowJs: true 并且无 .d.ts 文件时: 编辑器不报错,编译后可以正常运行, 但 .ts 引入 .js 文件的方法/函数没有类型提醒,参数是默认的 any 类型. 这种情况下可以使用 jsDoc 来让编辑带有类型提示功能.例如:
// src/js/index.js
/**
 *测试js文件编译
 *
 * @export
 * @param {string} name 名字
 * @returns 打招呼
 */
export function testJs(name) {
  name = 'hello' + name
  return name
}

此时, 当 testJs(123) 传入数字类型时, 编辑器会用红色下划线标识传入数据类型错误, 但是 tsc 命令仍然可以编译通过, 因此 jsDoc 方式只是让编辑器报错,并不能阻止编译过程.

  • allowJS: true 并且存在 .d.ts 文件时, 编辑器不报错,编译后可以正常运行, 类型提示使用 .d.ts 内定义的类型(不再使用js文件的jsDoc)
// src/js/index.d.ts
/**
 *测试js文件的编译
 *
 * @export
 * @param {string} name 名字
 * @returns {string} 返回打招呼
 */
export function testJs(name: string): string 

此时, 当 testJs(123) 传入数字类型时, 编辑器会用红色下划线标识传入数据类型错误, 同时 tsc 命令编译时不通过,编译报错如下:

E:\demo_ts>tsc
src/index.ts:3:20 - error TS2345: Argument of type '123' is not assignable to parameter of type 'string'.

3 console.log(testJs(123))
                     ~~~


Found 1 error.

这能帮我们更好的理解 .d.ts 文件的作用.

.d.ts 文件的理解

通过上文"配置项 allowJs"章节中示例代码, 让我们简单了解到了 .d.ts 的作用, .d.ts 文件并不具有"执行"功能, 只是真实业务代码(js 文件)的声明文件. 它解决了源js文件,在 typescript 编译过程中因为类型检测失败报错的问题(注意:只是编译阶段才有用).

在模块化编程的项目中, 我们可以为每个模块都定义一个 .d.ts 文件(.d.ts中使用顶级的 export声明), 例如上面的例子. 但我们其实可以把所有的模块声明在一个 .d.ts 文件中, 官网文档中其实也是建议这种形式的. 见模块/外部模块章节:

我们可以使用顶级的 export声明来为每个模块都定义一个 .d.ts 文件,但最好还是写在一个大的 .d.ts 文件里。 我们使用与构造一个外部命名空间相似的方法,但是这里使用 module关键字并且把名字用引号括起来,方便之后import

这里我们可以知道, .d.ts 并不是必须要依赖源模块文件的, 也不是随模块文件的加载而加载的. 我们平时把模块文件和 .d.ts 文件一一对应只是为了项目的方便管理, 并不是 typescript 的要求.

通过 declare module 合并声明的语法见下文 "declare 的使用"章节.

关于 .d.ts 放的位置

一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将 jQuery.d.ts(以jQuery为例, 通过script 标签全局引入) 放到项目中时,其他所有 *.ts 文件就都可以获得 jQuery 的类型定义了。

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

假如仍然无法解析,那么可以检查下 tsconfig.json 中的 files、includeexclude 配置.

实时上, 我们甚至可以不书写 .d.ts 文件, 把类型声明直接写到 ts 文件中, 以上文 jQuery 全局引入为例:

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

jQuery('#foo');
or
$('#foo')

核心概念 (摘自中文网)

类型

类型通过以下方式引入:

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

以上每种声明形式都会创建一个新的类型名称。

与类型相比,你可能已经理解了什么是值。 值是运行时名字,可以在表达式里引用。 比如 let x = 5;创建一个名为x的值。

同样,以下方式能够创建值:

  • let,const,和var声明
  • 包含值的namespace或module声明
  • enum声明
  • class声明
  • 指向值的import声明
  • function声明

命名空间

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

由上面类型/值的创建方式可知, 命名空间是属于创建值的方式,而不是类型的创建方式

简单的组合:一个名字,多种意义

一个给定的名字A,我们可以找出三种不同的意义:一个类型,一个值或一个命名空间。 要如何去解析这个名字要看它所在的上下文是怎样的。 比如,在声明 let m: A.A = A;, A首先被当做命名空间,然后做为类型名,最后是值。 这些意义最终可能会指向完全不同的声明!

内置组合

眼尖的读者可能会注意到,比如,class同时出现在类型和值列表里。 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);
这可以很好地工作,但是我们知道SomeType和SomeVar很相关 因此我们想让他们有相同的名字。 我们可以使用组合通过相同的名字 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类型 -- 它们是独立的。

declare的使用

.d.ts文件中使用declare 来声明变量的类型, 能用在全局命名空间(全局声明)或者包声明文件(声明一个局部变量)中, 这个声明仅仅用于编译时的检查,在编译结果中会被删除.

声明全局变量

declare var foo: number;
declare const foo: number;
declare let foo: number;

声明全局函数

declare function greet(greeting: string): void;

declare namespace 描述用点表示法访问的类型或值(对象)

注意 namespace 内代码的写法和在全局变量下是一样的, 也是写 function, let

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

// 代码中使用
let result = myLib.makeGreeting("hello, world");
console.log("The computed greeting is:" + result);

let count = myLib.numberOfGreetings;

declare module 声明模块之一

在书写模块插件.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;
    }
}

declare module 声明模块之二

在前端工程中,import 很多非 js 资源,例如:css, html, 图片,vue, 这种 ts 无法识别的资源时,就需要告诉ts,怎么识别这些导入的资源的类型。

// 看看vue怎么处理的:shims-vue.d.ts
declare module '*.vue' {
 import Vue from 'vue';
 export default Vue;
}
 
// html
declare module '*.html';
// css
declare module '*.css';

declare module 声明模块之三

和上文"之二"有类似效果, 可以认为都是"模块补充"

// 声明合并效果
// vue的声明在 vue/types/vue.d.ts
import Vue from 'vue'
declare module 'vue/types/vue' {
    // 相当于Vue.$eventBus
    interface Vue { 
        $eventBus: Vue;
    }
    // 相当于在Vue.prototype.$eventBus  即全局属性
    interface VueConstructor {
        $eventBus: Vue;
    }
}

// 声明vue中额外的组件选项
// ComponentOptions 声明于 types/options.d.ts 之中
declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    myOption?: string
  }
}

declare module 声明模块之四

用于外部模块的统一声明, 即把所有模块的声明写到一个 .d.ts 文件中(理解见上文".d.ts文件的理解")

// node.d.ts
declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }

    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}

declare module "path" {
    export function normalize(p: string): string;
    export function join(...paths: any[]): string;
    export let sep: string;
}

外部模块简写: 假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。

// declarations.d.ts
// 简写模块里所有导出的类型将是any
declare module "hot-new-module";

//---------
// ts文件中引入模块时
import x, {y} from "hot-new-module";
x(y);

模块声明通配符: 某些模块加载器如 SystemJSAMD支持导入非 JavaScript内容。 它们通常会使用一个前缀或后缀来表示特殊的加载语法。 模块声明通配符可以用来表示这些情况。

// xxx.d.ts
declare module "*!text" {
    const content: string;
    export default content;
}
// Some do it the other way around.
declare module "json!*" {
    const value: any;
    export default value;
}

// ---------
//现在你可以就导入匹配"*!text"或"json!*"的内容了。
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);

注意: 没有 declare interface 的写法, 需要声明接口直接写 interface,或者在命名空间中 export interface 即可!

理解 namespace

命名空间: 作为全局命名空间的子空间存在. 在书写 .d.ts时:

  • 可以通过 declare 声明
  • 书写 namespacke 内部的代码时和写全局命名空间一样.例如可以写 export, var 等, 而不是因为命名空间后面有 {} 就认为是对象(在非 .d.ts 文件内可以认为是对象)
// module-class.d.ts 类模块的声明文件

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

最好的关于文件声明的资料

点击这里

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342