关于类型声明文件 - 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、include
和 exclude
配置.
实时上, 我们甚至可以不书写 .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);
模块声明通配符: 某些模块加载器如 SystemJS
和 AMD
支持导入非 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;
}
}