在 《从 JavaScript 到 TypeScript 系列》 文章我们已经学习了 TypeScript 相关的知识。
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)",发布到
中,配合编辑器(或插件),就能够检测到 JS 库中的静态类型。
- npm
类型定义文件的以
结尾,里面主要用来定义类型。
- .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;
- }
编写起来非常简单,经过 TypeScript 良好的静态类型系统洗礼过后,语法学习成本非常低。
- *.d.ts
我们可以使用
用来定义类型变量:
- 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;
- }
当然,我们也可以使用
定义我们的复杂类型,在 TS 中我们也可以直接定义
- interface
:
- interface
- interface Application {
- init(): void
- get(key: string): object
- }
和
- interface
(或者说
- type
) 很像。
- class
但是
的含义是定义自定义类型,当 TS 提供给你的基础类型都不满足的时候,可以使用
- type
自由组合出你的新类型,而
- type
应该是对外输出的接口。
- interface
不可以被继承,但
- type
可以:
- interface
- interface BaseApplication {
- appId: number
- }
- export interface Application extends BaseApplication {
- init(): void
- get(key: string): object
- }
可以创建
- 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
输出到外部环境,子命名空间不需要
- 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
来遍历类型,例如上面的 ChinaMobile 例子,我们可以使用
- keyof
来约束属性名必须为三家运营商之一:
- 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
前者,安装完了包之后会自动检测并识别类型定义文件。
后者,则需要通过
安装,这就是我们前面所说的 DefinitelyTyped ,用于扩展 JS 库的类型声明。
- npm i @types/xxxx
内置类型定义就是把你的类型定义文件和 npm 包一起发布,一般来说,类型定义文件都放在包根目录的
目录里,例如 vue:
- types
如果你的包有一个主
文件,需要在
- .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 中。例如 @types/express。
- @types organizatio
根据
的规则,和编辑器(和插件) 自动检测静态类型。
- DefinitelyTyped
@types 下面的包是从 DefinitelyTyped 里自动发布的,通过 types-publisher 工具。
如果想让你的包发布为 @types 包,需要提交一个 pull request 到 https://github.com/DefinitelyTyped/DefinitelyTyped。
在这里查看详细信息 contribution guidelines page。
如果你正在使用 TypeScript,而使用了一些 JS 包并没有对应的类型定义文件,可以编写一份然后提交到
。
- @types
赠人玫瑰,手留余香。
发布到
的包可以通过 TypeSearch 搜索检索,使用
- @types organizatio
安装:
- npm install --save-dev @types/xxxx
更多细节请参阅 DefinitelyTyped。
通常来说,如果这份类型定义文件是 JS 库自带的,那么我们可以直接导出模块:
- interface User {}
- export = User
而如果这份类型定义文件不是 JS 库自带的,而是第三方的,则需要使用
进行关联。
- module
例如
发布的 npm 包中不包含
- jquery
类型定义文件,
- *.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
- }
来源: http://www.cnblogs.com/silin6/p/7793753.html