JavaScript 和 TypeScript 交叉口 —— 类型定义文件(*.d.ts)
2017-11-06 16:22
916 查看
在 《从 JavaScript 到 TypeScript 系列》 文章我们已经学习了 TypeScript 相关的知识。
TypeScript 的核心在于静态类型,我们在编写 TS 的时候会定义很多的类型,但是主流的库都是 JavaScript 编写的,并不支持类型系统。那么如何让这些第三方库也可以进行类型推导呢?
这篇文章我们来讲解 JavaScript 和 TypeScript 的静态类型交叉口 —— 类型定义文件。
这篇文章首发于我的个人博客 《听说》。
前端开发 QQ 群:377786580
但是主流的库都是 JavaScript 编写的,TypeScript 身为 JavaScript 的超集,自然需要考虑到如何让 JS 库也能定义静态类型。
TypeScript 经过了一系列的摸索,先后提出了 tsd(已废弃)、typings(已废弃),最终在 TypeScript 2.0 的时候重新整理了类型定义,提出了 DefinitelyTyped。
DefinitelyTyped 就是让你把 "类型定义文件(*.d.ts)",发布到
类型定义文件的以
例如这是 jQuery 的类型定义文件 中一段代码(为了方便理解做了一些改动)
我们可以使用
可以看到
当然,我们也可以使用
但是
基本上顶层的定义都需要使用
我们可以使用动态属性名来定义类型:
我们也可以用
与你的 npm 包捆绑在一起(内置类型定义文件)
发布到 npm 上的 @types organization
前者,安装完了包之后会自动检测并识别类型定义文件。
后者,则需要通过
如果你的包有一个主
设置
如果主类型定义文件名是
如果你发的包中,
如果只发二级目录的话,把类型定义文件放到对应的二级目录下即可:
根据
@types 下面的包是从 DefinitelyTyped 里自动发布的,通过 types-publisher 工具。
如果想让你的包发布为 @types 包,需要提交一个 pull request 到 https://github.com/DefinitelyTyped/DefinitelyTyped。
在这里查看详细信息 contribution guidelines page。
如果你正在使用 TypeScript,而使用了一些 JS 包并没有对应的类型定义文件,可以编写一份然后提交到
赠人玫瑰,手留余香。
发布到
更多细节请参阅 DefinitelyTyped。
而如果这份类型定义文件不是 JS 库自带的,而是第三方的,则需要使用
例如
从而解决了一些主流的 JS 库发布的
入口模块主要做这些事情:
定义命名空间
导出和聚合子模块
主出口文件
子模块无需定义命名空间,这样外部环境 (
TypeScript 的核心在于静态类型,我们在编写 TS 的时候会定义很多的类型,但是主流的库都是 JavaScript 编写的,并不支持类型系统。那么如何让这些第三方库也可以进行类型推导呢?
这篇文章我们来讲解 JavaScript 和 TypeScript 的静态类型交叉口 —— 类型定义文件。
这篇文章首发于我的个人博客 《听说》。
前端开发 QQ 群:377786580
类型定义文件
在 TypeScript 中,我们可以很简单的,在代码编写中定义类型:interface IBaseModel { say(keys: string[] | null): object } class User implements IBaseModel { name: string constructor (name: string) { this.name = name } }
但是主流的库都是 JavaScript 编写的,TypeScript 身为 JavaScript 的超集,自然需要考虑到如何让 JS 库也能定义静态类型。
TypeScript 经过了一系列的摸索,先后提出了 tsd(已废弃)、typings(已废弃),最终在 TypeScript 2.0 的时候重新整理了类型定义,提出了 DefinitelyTyped。
DefinitelyTyped 就是让你把 "类型定义文件(*.d.ts)",发布到
npm中,配合编辑器(或插件),就能够检测到 JS 库中的静态类型。
类型定义文件的以
.d.ts结尾,里面主要用来定义类型。
例如这是 jQuery 的类型定义文件 中一段代码(为了方便理解做了一些改动)
// 定义 jQuery 需要用到的类型命名空间 declare namespace JQuery { // 定义基本使用的类型 type Selector = string; type TypeOrArray<T> = T | T[]; type htmlString = string; } // 定义 jQuery 接口,jquery 是一个 包含 Element 的集合 interface JQuery<TElement extends Node = HTMLElement> extends Iterable<TElement> { length: number; eq(index: number): this; // 重载 add(selector: JQuery.Selector, context: Element): this; add(selector: JQuery.Selector | JQuery.TypeOrArray<Element> | JQuery.htmlString | JQuery): this; children(selector?: JQuery.Selector): this; css(propertyName: string): string; html(): string; } // 对模块 jquery 输出接口 declare module 'jquery' { // module 中要使用 export = 而不是 export default export = jQuery; }
类型定义
*.d.ts编写起来非常简单,经过 TypeScript 良好的静态类型系统洗礼过后,语法学习成本非常低。
我们可以使用
type用来定义类型变量:
// 基本类型 type UserName = string // 类型赋值 type WebSite = string type Tsaid = WebSite
可以看到
type其实可以定义各种格式的类型,也可以和其他类型进行组合。
// 对象 type User = { name: string; age: number; website: WebSite; } // 方法 type say = (age: number) => string // 类 class TaSaid { website: string; say: (age: number) => string; }
当然,我们也可以使用
interface定义我们的复杂类型,在 TS 中我们也可以直接定义
interface:
interface Application { init(): void get(key: string): object }
interface和
type(或者说
class) 很像。
但是
type的含义是定义自定义类型,当 TS 提供给你的基础类型都不满足的时候,可以使用
type自由组合出你的新类型,而
interface应该是对外输出的接口。
type不可以被继承,但
interface可以:
interface BaseApplication { appId: number } export interface Application extends BaseApplication { init(): void get(key: string): object }
declare
declare可以创建
*.d.ts文件中的变量,
declare只能作用域最外层:
declare var foo: number; declare function greet(greeting: string): void; declare namespace tasaid { // 这里不能 declare interface blog { website: 'http://tasaid.com' } }
基本上顶层的定义都需要使用
declare,
class也是:
declare class User { name: string }
namespace
为防止类型重复,使用namespace用于划分区域块,分离重复的类型,顶层的
namespace需要
declare输出到外部环境,子命名空间不需要
declare。
// 命名空间 declare namespace Models { type A = number // 子命名空间 namespace Config { type A = object type B = string } } type C = Models.Config.A
组合定义
上面我们只演示了一些简单的类型组合,生产环境中会包含许多复杂的类型定义,这时候我们就需要各种组合出强大的类型定义:动态属性
有些类型的属性名是动态而未知的,例如:{ '10086': { name: '中国移动', website: 'http://www.10086.cn', }, '10010': { name: '中国联通', website: 'http://www.10010.com', }, '10000': { name: '中国电信', website: 'http://www.189.cn' } }
我们可以使用动态属性名来定义类型:
interface ChinaMobile { name: string; website: string; } interface ChinaMobileList { // 动态属性 [phone: string]: ChinaMobile }
类型遍历
当你已知某个类型范围的时候,可以使用in和
keyof来遍历类型,例如上面的 ChinaMobile 例子,我们可以使用
in来约束属性名必须为三家运营商之一:
type ChinaMobilePhones = '10086' | '10010' | '10000' interface ChinaMobile { name: string; website: string; } // 只能 type 使用, interface 无法使用 type ChinaMobileList = { // 遍历属性 [phone in ChinaMobilePhones]: ChinaMobile }
我们也可以用
keyof来约定方法的参数
export type keys = { name: string; appId: number; config: object; } class Application { // 参数和值约束范围 set<T extends keyof keys>(key: T, val: keys[T]) get<T extends keyof keys>(key: T): keys[T] }
集成发布
有两种主要方式用来发布类型定义文件到npm:
与你的 npm 包捆绑在一起(内置类型定义文件)
发布到 npm 上的 @types organization
前者,安装完了包之后会自动检测并识别类型定义文件。
后者,则需要通过
npm i @types/xxxx安装,这就是我们前面所说的 DefinitelyTyped ,用于扩展 JS 库的类型声明。
内置类型定义文件
内置类型定义就是把你的类型定义文件和 npm 包一起发布,一般来说,类型定义文件都放在包根目录的types目录里,例如 vue:
如果你的包有一个主
.js文件,需要在
package.json里指定主类型定义文件。
设置
types或
typeings属性指向捆绑在一起的类型定义文件。 例如包目录如下:
├── lib │ ├── main.js │ └── main.d.ts # 类型定义文件 └── package.json
// pageage.json { "name": "demo", "author": "demo project", "version": "1.0.0", "main": "./lib/main.js", // 定义主类型定义文件 "types": "./lib/main.d.ts" }
如果主类型定义文件名是
index.d.ts并且位置在包的根目录里,就不需要使用
types属性指定了。
├── lib │ └── main.js ├── index.d.ts # 类型定义文件 └── package.json
如果你发的包中,
package.json中使用了
files字段的话(
npm会根据
files配置的规则决定发布哪些文件),则需要手动把类型定义文件加入:
// pageage.json { "files": [ "index.js", "*.d.ts" ] }
如果只发二级目录的话,把类型定义文件放到对应的二级目录下即可:
import { default as App } from 'demo/app'
发布到 @types organizatio
发布到@types organizatio的包表示源包没有包含类型定义文件,第三方/或原作者定义好类型定义文件之后,发布到 @types 中。例如 @types/express。
根据
DefinitelyTyped的规则,和编辑器(和插件) 自动检测静态类型。
@types 下面的包是从 DefinitelyTyped 里自动发布的,通过 types-publisher 工具。
如果想让你的包发布为 @types 包,需要提交一个 pull request 到 https://github.com/DefinitelyTyped/DefinitelyTyped。
在这里查看详细信息 contribution guidelines page。
如果你正在使用 TypeScript,而使用了一些 JS 包并没有对应的类型定义文件,可以编写一份然后提交到
@types。
赠人玫瑰,手留余香。
发布到
@types organizatio的包可以通过 TypeSearch 搜索检索,使用
npm install --save-dev @types/xxxx安装:
更多细节请参阅 DefinitelyTyped。
其他
module
通常来说,如果这份类型定义文件是 JS 库自带的,那么我们可以直接导出模块:interface User {} export = User
而如果这份类型定义文件不是 JS 库自带的,而是第三方的,则需要使用
module进行关联。
例如
jquery发布的 npm 包中不包含
*.d.ts类型定义文件,
jquery的类型定义文件发布在了
@types/jquery,所以类型定义文件中导出类型的时候,需要关联模块
jquery,意思就是我专门针对这个包做的类型定义:
interface jQuery {} declare module 'jquery' { // module 中要使用 export = 而不是 export default export = jQuery; }
从而解决了一些主流的 JS 库发布的
npm包中没有类型定义文件,但是我们可以用第三方类型定义文件为这些库补充类型。
风格
经过一系列探索,个人比较推荐下面的编写风格,先看目录:types ├── application.d.ts ├── config.d.ts ├── index.d.ts # 入口模块 └── user.d.ts
入口模块主要做这些事情:
定义命名空间
导出和聚合子模块
主出口文件
index.d.ts:
import * as UserModel from './user' import * as AppModel from './application' import * as ConfigModel from './config' declare namespace Models { export type User = UserModel.User; export type Application = AppModel.Application; // 利用 as 抹平争议性变量名 export type Config = ConfigModel.Config; }
子模块无需定义命名空间,这样外部环境 (
types文件夹之外) 则无法获取子模块类型,达到了类型封闭的效果:
export interface User { name: string; age: number }
相关文章推荐
- 在Java中将数据库查询结果保存为List<Map>类型的JavaScript数组文件
- JavaScript实现清空(重置)文件类型INPUT元素值的方法
- xsl 文件如何定义 Javascript 函数并且调用
- 类模块中定义的变量能否同自定义类型那样作为文件记录存取?
- TypeScript 强类型 JavaScript – Rafy Web 框架选型
- ROS之msg文件定义以及自定义发布主题消息类型
- 自动根据视频文件类型选择不同播放器的JAVASCRIPT代码
- 基于Liferay的平台下,portlet在各个模式下分别加载以<footer-portlet-javascript>定义的js文件的不可行性 推荐
- 类型定义、数据定义放在头文件里?还是c文件?
- JavaScript使用prototype定义对象类型(转)[
- JavaScript中基础变量和数据类型的定义
- 03_MyBatis基本查询,mapper文件的定义,测试代码的编写,resultMap配置返回值,sql片段配置,select标签标签中的内容介绍,配置使用二级缓存,使用别名的数据类型,条件查询ma
- [译]JavaScript文件操作(5)-Bolb类型(二进制大对象)
- javascript验证上传文件的类型限制必须为某些格式
- JavaScript 文件上传类型判断
- javascript验证上传文件的类型限制必须为某些格式
- JavaScript判断上传文件类型
- 文件类型错误定义
- JavaScript限制上传文件类型的代码
- JavaScript限制上传文件类型的代码