TypeScript声明文件的语法与场景详解

目录
  • 简介
  • 语法
    • 内容
    • 模块化
      • 模块语法
      • 三斜线指令
      • reference
      • amd-module
  • 场景
    • 1. 在内部项目中给内部项目写声明文件
    • 2. 给第三方包写声明文件
      • 全局变量的第三方库
      • 修改全局变量的模块的第三方库的声明
      • 修改window
      • ESM和CommonJS
      • UMD
      • 模块插件
  • 总结

简介

声明文件是以.d.ts为后缀的文件,开发者在声明文件中编写类型声明,TypeScript根据声明文件的内容进行类型检查。(注意同目录下最好不要有同名的.ts文件和.d.ts,例如lib.ts和lib.d.ts,否则模块系统无法只根据文件名加载模块)

为什么需要声明文件呢?我们知道TypeScript根据类型声明进行类型检查,但有些情况可能没有类型声明:

  • 第三方包,因为第三方包打包后都是JavaScript语法,而非TypeScript,没有类型。
  • 宿主环境扩展,如一些hybrid环境,在window变量下有一些bridge接口,这些接口没有类型声明。

如果没有类型声明,在使用变量、调用函数、实例化类的时候就没法通过TypeScript的类型检查。

声明文件就是针对这些情况,开发者在声明文件中编写第三方模块的类型声明/宿主环境的类型声明。让TypeScript可以正常地进行类型检查。

除此之外,声明文件也可以被导入,使用其中暴露的类型定义。

总之,声明文件有两种用法:

  • 被通过import导入,使用其中暴露的类型定义和变量声明。
  • 和相关模块关联,为模块进行类型声明。

对于第二种用法,声明文件如何同相关模块关联呢?

比如有个第三方包名字叫"foo",那么TypeScript会在node_modules/foo中根据其package.json的types和typing字段查找声明文件查找到的声明文件被作为该模块的声明文件;TypeScript也会在node_modules/@types/foo/目录中查找声明文件,如果能找到就被作为foo模块的声明文件;TypeScript还会在我们的项目中查找.d.ts文件,如果遇到declare module 'foo'语句,则该声明被用作foo模块的声明。

总结一下,TypeScript会在特定的目录读取指定的声明文件。

  • 在内部项目中,TypeScript会读取tsconfig.json中的文件集合,在其中的声明文件才会被处理。
  • 读取node_modules中各第三方包的package.json的types或者typing指定的文件。
  • 读取@types目录下同名包的声明文件。

声明文件中的代码不会出现在最终的编译结果中,编译后会把转换后的JavaScript代码输出到"outDir"选项指定的目录中,并且把 .ts模块中使用到的值的声明都输出到"declarationDir"指定的目录中。

而在.ts文件中的声明语句,编译后会被去掉,如

declare let a: number;

export default a;

会被编译为

"use strict";
exports.__esModule = true;
exports["default"] = a;

TypeScript编译过程不仅将TypeScript语法转译为ES6/ES5,还会将代码中.ts文件中用到的值的类型输出到指定的声明文件中。如果你需要实现一个库项目,这个功能很有用,因为用到你的库的项目可以直接使用这些声明文件,而不需要你再为你的库写声明文件。

语法

内容

TypeScript中的声明会创建以下三种实体之一:命名空间,类型或值。

命名空间最终被编译为全局变量,因此我们也可以认为声明文件中其实创建了类型和值两种实体。即定义类型或者声明值。

// 类型 接口
interface Person {name: string;}

// 类型 类型别名
type Fruit = {size: number};

// 值 变量
declare let a: number;

// 值 函数
declare function log(message: string): void;

// 值 类
declare class Person {name: string;}

// 值 枚举
declare enum Color {Red, Green}

// 值 命名空间
declare namespace person {let name: string;}

我们注意到类型可以直接定义,但是值的声明需要借助declare关键字,这是因为如果不用declare关键字,值的声明和初始化是一起的,如

let a: number;

// 编译为
var a;

但是编译结果是会去掉所有的声明语句,保留初始化的部分,而声明文件中的内容只是起声明作用,因此需要通过declare来标识,这只是声明语句,编译时候直接去掉即可。

TypeScript也约束声明文件中声明一个值必须要用declare,否则会被认为存在初始化的内容,从而报错。

// foo.d.ts
let a: number = 1; // error TS1039: Initializers are not allowed in ambient contexts.

declare也允许出现在.ts文件中,但一般不会这么做,.ts文件中直接用let/const/function/class就可以声明并初始化一个变量。并且.ts文件编译后也会去掉declare的语句,所以不需要declare语句。

注意,declare多个同名的变量是会冲突的

declare let foo: number; // error TS2451: Cannot redeclare block-scoped variable 'a'.

declare let foo: number; // error TS2451: Cannot redeclare block-scoped variable 'a'.

除了使用declare声明一个值,declare还可以用来声明一个模块和全局的插件,这两种用法都是在特定场景用来给第三方包做声明。

declare module用来给一个第三方模进行类型声明,比如有一个第三方包foo,没有类型声明。我们可以在我们项目中实现一个声明文件来让TypeScript可以识别模块类型:foo.d.ts

// foo.d.ts
declare module 'foo' {
    export let size: number;
}

然后我们就可以使用了:

import foo from 'foo';

console.log(foo.size);

declare module除了可以用来给一个模块声明类型,还可以用来实现模块插件的声明。后面小节中会做介绍。

declare global用来给扩展全局的第三方包进行声明,后面小节介绍。

模块化

模块语法

声明文件的模块化语法和.ts模块的类似,在一些细节上稍有不同。.ts导出的是模块(typescript会根据导出的模块判断类型),.d.ts导出的是类型的定义和声明的值。

声明文件可以导出类型,也可以导出值的声明

// index.d.ts

// 导出值声明
export let a: number;

// 导出类型
export interface Person {
    name: string;
};

声明文件可以引入其他的声明文件,甚至可以引入其他的.ts文件(因为.ts文件也可能导出类型)

// Person.d.ts
export default interface Person {name: string}

// index.d.ts
import Person from './person';

export let p: Person;

如果声明文件不导出,默认是全局可以访问的

// person.d.ts
interface Person {name: string}
declare let p: Person;

// index.ts
let p1: Person = {name: 'Sam'};
console.log(p);

如果使用模块导出语法(ESM/CommJS/UMD),则不解析为全局(当然UMD还是可以全局访问)。

// ESM

interface Person {name: string}

export let p: Person;

export default Person;
// CommonJS
interface Person {name: string}

declare let p: Person;

export = p;
// UMD
interface Person {name: string}

declare let p: Person;

export = p;
export as namespace p;

注意:UMD包export as namespace语法只能在声明文件中出现。

三斜线指令

声明文件中的三斜线指令,用于控制编译过程。

三斜线指令仅可放在包含它的文件的最顶端。

如果指定--noResove编译选项,预编译过程会忽略三斜线指令。

reference

reference指令用来表明声明文件的依赖情况。

/// <reference path="..." />用来告诉编译器依赖的其他声明文件。编译器预处理时候会将path指定的声明文件加入进来。路径是相对于文件自身的。引用不存在的文件或者引用自身,会报错。

/// <reference types="node" />用来告诉编译器它依赖node_modules/@types/node/index.d.ts。如果你的项目里面依赖了@types中的某些声明文件,那么编译后输出的声明文件中会自动加上这个指令,用以说明你的项目中的声明文件依赖了@types中相关的声明文件。

/// <reference no-default-lib="true"/>,

这涉及两个编译选项,--noLib,设置了这个编译选项后,编译器会忽略默认库,默认库是在安装TypeScript时候自动引入的,这个文件包含 JavaScript 运行时(如window)以及 DOM 中存在各种常见的环境声明。但是如果你的项目运行环境和基于标准浏览器运行时环境有很大不同,可能需要排除默认库,一旦你排除了默认的 lib.d.ts 文件,你就可以在编译上下文中包含一个命名相似的文件,TypeScript 将提取该文件进行类型检查。

另一个编译选项是--skipDefaultLibCheck这个选项会让编译器忽略包含了/// <reference no-default-lib="true"/>指令的声明文件。你会注意到在默认库的顶端都会有这个三斜线指令,因此如果采用了--skipDefaultLibCheck编译选项,也同样会忽略默认库。

amd-module

amd-module相关指令用于控制打包到amd模块的编译过程

///<amd-module name='NamedModule'/>这个指令用于告诉编译器给打包为AMD的模块传入模块名(默认情况是匿名的)

///<amd-module name='NamedModule'/>
export class C {
}

编译结果为

define("NamedModule", ["require", "exports"], function (require, exports) {
    var C = (function () {
        function C() {
        }
        return C;
    })();
    exports.C = C;
});

场景

这里我们将自己的项目代码称为“内部项目”,引入的第三方模块,包括npm引入的和script引入的,称为“外部模块”。

1. 在内部项目中给内部项目写声明文件

自己项目中,给自己的模块写声明文件,例如多个模块共享的类型,就可以写一个声明文件。这种场景通常不必要,一般是某个.ts文件导出声明,其他模块引用声明。

2. 给第三方包写声明文件

给第三方包写声明文件又分为在内部项目中给第三方包写声明文件和在外部模块中给外部模块写声明文件。

在内部项目中给第三方包写声明文件: 如果第三方包没有TS声明文件,则为了保证使用第三方包时候能够通过类型检查,也为了安全地使用第三方包,需要在内部项目中写第三方包的声明文件。

在外部模块中给外部模块写声明文件: 如果你是第三方库的作者,无论你是否使用TypeScript开发库,都应该提供声明文件以便用TypeScript开发的项目能够更好地使用你的库,那么你就需要写好你的声明文件。

这两种情况的声明文件的语法类似,只在个别声明语法和文件的处理上有区别:

  • 内部项目给第三方包写声明文件时候,以.d.ts命名即可,然后在tsconfig.json中的files和include中配置能够包含到文件即可,外部模块的声明文件需要打包到输出目录,并且在package.json中的type字段指定声明文件位置;或者上传到@types/<moduleName>中,使用者通过npm install @types/<moduleName>安装声明文件。redux就在tsconfig.json中指定了declarationDir为./types,TypeScript会将项目的声明都打包到这个目录下,目录结构和源码一样,然后redux源码入口处导出了所有的模块,因此types目录下也有一个入口的声明文件index.d.ts,并且包含了所有的导出模块声明,redux在package.json中指定types字段(或者typings字段)为入口的声明文件:./types/index.d.ts。这样就实现了自动生成接口的声明文件。
  • 内部项目给第三方写声明文件时候,如果是通过npm模块引入方式,如import moduleName from 'path';则需要通过declare module '<moduleName>'语法来声明模块。而外部模块的声明文件都是正常的类型导出语法(如export default export =等),如果声明文件在@types中,会将与模块同名的声明文件作为模块的类型声明;如果声明文件在第三方包中,那么就TypeScript模块就将它作为这个第三方包模块的模块声明,当使用者导入并使用这个模块时候,TypeScript就根据相应地声明文件进行类型提示和类型检查。

根据第三方包类型可以分成几种

全局变量的第三方库

我们知道如果不使用模块导出语法,声明文件默认的声明都是全局的。

declare namespace person {
    let name: string
}

或者

interface Person {
    name: string;
}

declare let person: Person;

使用:

console.log(person.name);

修改全局变量的模块的第三方库的声明

如果有第三方包修改了一个全局模块(这个第三方包是这个全局模块的插件),这个第三方包的声明文件根据全局模块的声明,有不同的声明方式

如果全局模块使用命名空间声明

declare namespace person {
    let name: string
}

根据命名空间的声明合并原理,插件模块可以这样声明

declare namespace person {
  	// 扩展了age属性
    let age: number;
}

如果全局模块使用全局变量声明

interface Person {
    name: string;
}

declare let person: Person;

根据接口的声明合并原理,插件模块可以这样声明

interface Person {
  	// 扩展了age属性
    age: number;
}

上面的全局模块的插件模块的声明方式可以应用于下面的场景:

  • 内部项目使用了插件,但插件没有声明文件,我们可以在内部项目中自己实现声明文件。
  • 给插件模块写声明文件并发布到@types。

如果是插件模块的作者,希望在项目中引用全局模块并且将扩展的类型输出到声明文件,以便其他项目使用。可以这样实现

// plugin/index.ts

// 注意这样声明才会让TypeScript将类型输出声明文件
declare global {
  	// 假设全局模块使用全局变量的方式声明
    interface Person {
        age: number
    }
}

console.log(person.age);

export {};

注意,declare global写在声明文件中也可以,但是要在尾部加上export {}或者其他的模块导出语句,否则会报错。另外declare global在声明文件中写的话,编译后不会输出到声明文件中。

修改window

window的类型是interface Window {...},在默认库中声明,如果要扩展window变量(如一些hybrid环境)可以这样实现

// window.d.ts

// 声明合并
interface Window {
		bridge: {log(): void}
}

// 或者
declare global {
    interface Window {
        bridge: {log(): void}
    }
}

或者

// index.ts

declare global {
    interface Window {
        bridge: {log(): void}
    }
}

window.bridge = {log() {}}

export {};

ESM和CommonJS

给第三方的ESM或者CommonJS模块写声明文件,使用ESM导出或者CommonJS模块语法导出都可以,不管第三方包是哪种模块形式。

看下面示例

interface Person {
    name: string;
}

declare let person: Person;

export = person;
// 也可以使用export default person;
import person from 'person';

console.log(person.name);

上面的声明文件是放在node_modules/@types/person/index.d.ts中,或者放在node_modules/person/package.json的types或者typings字段指定的位置。

如果在自己项目中声明,应该使用declare module实现

declare module 'person' {
    export let name: string;
}

UMD

UMD模块,在CommonJS声明的基础上加上export as namespace ModuleName;语句即可。

看下面的ESM的例子

// node_modules/@types/person/index.d.ts
interface Person {
    name: string;
}

declare let person: Person;

export default person;

export as namespace person;

可以通过import导入来访问

// src/index.ts
import person from 'person';

console.log(person.name);

也可以通过全局访问

// src/index.ts

// 注意如果用ESM导出,全局使用时候先访问defalut属性。
console.log(person.default.name);

下面是CommonJS的例子

// node_modules/@types/person/index.d.ts

interface Person {
    name: string;
}

declare let person: Person;

export default person;

export as namespace person;

可以通过import引入访问

// src/index.ts

import person from 'person';

console.log(person.name);

也可以全局访问

// src/index.ts

console.log(person.name);

模块插件

上面我们提到,declare module不仅可以用于给一个第三方模块声明类型,还可以用来给第三方模块的插件模块声明类型。

// types/moment-plugin/index.d.ts

// 如果moment定义为UMD,就不需要引入,直接能够使用
import * as moment from 'moment';

declare module 'moment' {
    export function foo(): moment.CalendarKey;
}

// src/index.ts

import * as moment from 'moment';
import 'moment-plugin';

moment.foo();

比如作为redux的插件的redux-thunk的声明文件extend-redux.d.ts,就是这样声明的

// node_modules/redux-thunk/extend-redux.d.ts

declare module 'redux' {
		// declaration code......
}

总结

到此这篇关于TypeScript声明文件的语法与场景详解的文章就介绍到这了,更多相关TS声明文件内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 如何获取TypeScript的声明文件.d.ts

    一.TypeScript的声明文件就像C/C++用.h文件.当使用TypeScript调用其他已经编写好的类库时,可以提供IntelliSense智能提示. 二.使用npm指令来获取.d.ts文件 install -save @types/jquery -g 运行后在type/jquery目录下生成4个文件:index.d.ts,lincense,package.json,readme.md,其中的index.d.ts即为jquery的声明文件. 三.npm需要Nodejs环境,安装步骤如下 1

  • js捆绑TypeScript声明文件的方法教程

    前话 TypeScript是JavaScript类型的超集,这是TypeScript的文档介绍的一句话,那么他们存在联系呢? 我的理解是,TypeScript在JavaScript基础上引入强类型语言的特性.开发者使用TypeScript语法进行编程开发,最终通过转换工具将TypeScript转换成JavaScript. 使用TypeScript能够避免在原生JavaScript上开发所带来的弱类型语言的坑.(我该输入啥?调用后返回啥?哎还是看看源码吧...) 嗯!很好,强类型的JavaScri

  • OGNL表达式基本语法与用法详解

    一.OGNL中的#.%和$符号 #.%和$符号在OGNL表达式中经常出现,而这三种符号也是开发者不容易掌握和理解的部分.在这里我们简单介绍它们的相应用途. 1.#符号的三种用法 1)访问非根对象属性,例如示例中的#session.msg表达式,由于Struts 2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀.实际上,#相当于ActionContext. getContext():#session.msg表达式相当于ActionContext.getContext().getSessi

  • 关于Java变量的声明、内存分配及初始化详解

    实例如下: class Person { String name; int age; void talk() { System.out.println("我是: "+name+", 今年: "+age+"岁"); } } public class TestJava2_1 { public static void main(String args[]) { Person p; if (p == null) { p = new Person(); }

  • C语言在头文件中定义const变量详解

    C语言在头文件中定义const变量详解 在头文件中定义const不会有多变量的警告或错误,如果该头文件被大量包含会造成rom空间的浪费. 通过查看*.i文件的展开呢,可以发现每个.i文件都会有相应的变量展开. 查看*.map文件,能查看到该变量的多个地址分配. 在预编译的时候如果在头文件定义了const变量,每一个包含该头文件的c文件都会将其展开,而在编译的时候不会报错,因为这符合语法规则,每一个包含这个头文件的*.c文件都会编译一次这个变量,分配一个新的地址,然后在链接的时候也不会报错,因为每

  • python函数声明和调用定义及原理详解

    这篇文章主要介绍了python函数声明和调用定义及原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 函数是指代码片段,可以重复调用,比如我们前面文章接触到的type()/len()等等都是函数,这些函数是python的内置函数,python底层封装后用于实现某些功能. 一.函数的定义 在Python中,定义一个函数要使用def语句,依次写出函数名.括号.括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回:

  • Vue vm.$attrs使用场景详解

    1.vm.$attrs简介 首先我们来看下vue官方对vm.$attrs的介绍: 包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外).当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件--在创建更高层次的组件时非常有用. 猛一看有点看不明白.... 2.场景介绍 vue中一个比较令人烦恼的事情是属性只能从父组件传递给子组件

  • 关于正则表达式基本语法的应用详解(必看篇)

    1.正则表达式基本语法 两个特殊的符号'^'和'$'.他们的作用是分别指出一个字符串的开始和结束.例子如下: "^The":表示所有以"The"开始的字符串("There","The cat"等): "of despair$":表示所以以"of despair"结尾的字符串: "^abc$":表示开始和结尾都是"abc"的字符串--呵呵,只有&qu

  • php判断文件上传图片格式的实例详解

    php判断文件上传图片格式的实例详解 判断文件图片类型, $type = $_FILES['image']['tmp_name'];//文件名 //$type = $this->getImagetype( $type ); $filetype = ['jpg', 'jpeg', 'gif', 'bmp', 'png']; if (! in_array($type, $filetype)) { return "不是图片类型"; } 如上如果用户修改文件后缀为png jpeg等无法满

  • Linux ftp 命令行中下载文件get与上传文件put的命令应用详解

    介绍:从本地以用户anok登录的机器192.168.0.16上通过ftp远程登录到192.168.0.6的ftp服务器上,登录用户名是peo.以下为使用该连接做的实验. 查看远程ftp服务器上用户peo相应目录下的文件所使用的命令为:ls,登录到ftp后在ftp命令提示符下查看本地机器用户anok相应目录下文件的命令是:!ls.查询ftp命令可在提示符下输入:?,然后回车. 1.从远程ftp服务器下载文件的命令格式: get  远程ftp服务器上当前目录下要下载的文件名  [下载到本地机器上当前

  • Java Spring MVC 上传下载文件配置及controller方法详解

    下载: 1.在spring-mvc中配置(用于100M以下的文件下载) <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <!--配置下载返回类型--> <bean class="or

随机推荐

其他