typescript-declaration
TypeScript Declaration Files 深度指南
什么是 Declaration Files?
TypeScript 的 declaration files(声明文件)是以 .d.ts 为扩展名的文件。它们的主要作用是为 JavaScript 代码提供类型信息。
核心作用
1. 为 JavaScript 库提供类型支持
- 很多 JavaScript 库本身没有类型信息
- Declaration files 让 TypeScript 项目能够对这些库进行类型检查
- 使用这些库时会获得代码补全和类型提示
2. 定义类型,不包含实现
.d.ts文件只声明类型、接口、类的结构,不包含实现代码- 实际的逻辑代码在对应的
.js文件中
.d.ts 文件的本质
从 TypeScript 编译器角度
.d.ts 文件和普通的 .ts 文件在语法上没有本质区别。 命名只是约定俗成:
- 都是 TypeScript 代码
- 都支持类型注解、接口、类型别名等所有 TypeScript 特性
- 编译器对两者的处理方式相同
- 命名(
.d.ts)只是一个约定,用来表达"这是一个声明文件"的意图
实际区别在于使用场景
// 普通 .ts 文件:包含实现逻辑
// utils.ts
export function add(a: number, b: number): number {
return a + b;
}
// .d.ts 文件:通常只包含类型声明
// utils.d.ts
export function add(a: number, b: number): number;
编译器并不强制要求使用 .d.ts 扩展名,但这个约定帮助开发者快速识别这是一个声明文件。
declare 关键字的作用
为什么需要 declare?
declare 关键字告诉 TypeScript 编译器:"这个东西存在,但它的实现在别处(通常是 JavaScript 运行时),我只是在这里声明它的类型"
declare 的核心作用:区分声明和实现
// 不用 declare:TypeScript 期望这里有实现
export function add(a: number, b: number): number {
return a + number; // 必须有实现
}
// 用 declare:只声明,不需要实现
declare export function add(a: number, b: number): number;
declare 的常见用法
1. 声明全局变量
// 某个外部 JS 库在全局挂了一个变量
declare const API_KEY: string; // 告诉 TS 这个全局变量存在
2. 声明全局函数
declare function fetch(url: string): Promise<Response>;
3. 声明类
declare class User {
name: string;
constructor(name: string);
}
4. 声明模块
// 告诉 TS"这个模块存在",即使 node_modules 里没有类型
declare module 'my-untyped-lib' {
export function doSomething(): void;
}
5. 声明命名空间
declare namespace jQuery {
function ajax(settings: any): any;
}
实际例子:jQuery
假设你用一个通过 标签引入的 jQuery:
<script src="jquery.js"></script>
在 TypeScript 中使用它:
// ❌ 不行:TS 不知道 $ 是什么
$('#myId').text('hello');
// ✅ 可以:声明 $ 的存在
declare const $: any;
$('#myId').text('hello');
// ✅ 更好:声明具体的类型
declare const $: {
(selector: string): { text(content: string): void };
};
$('#myId').text('hello');
declare 与 interface 的区别
核心区别
| 关键字 | 用途 | 能声明什么 |
|--------|------|----------|
| interface | 定义对象/类的结构 | 只能定义对象结构 |
| declare | 告诉 TS "这个东西存在" | 函数、变量、类、模块、命名空间等任何东西 |
interface 的局限
// interface 只能描述对象的结构
interface User {
name: string;
age: number;
}
// ✅ 可以作为类型使用
const user: User = { name: 'Tom', age: 18 };
// ❌ 不能用 interface 声明函数
interface add(a: number, b: number): number; // 语法错误
declare 的灵活性
// 声明函数
declare function add(a: number, b: number): number;
// 声明变量
declare const API_KEY: string;
// 声明类
declare class User {
name: string;
constructor(name: string);
}
// 声明模块
declare module 'my-lib' {
export function doSomething(): void;
}
// 声明命名空间
declare namespace jQuery {
function ajax(settings: any): any;
}
场景对比
场景 1:外部 JavaScript 库的全局函数
// HTML 中引入的库
<script src="library.js"></script>
// ❌ interface 不行:interface 不能声明函数
interface myFunc { // 这不是你想要的
(a: number): number;
}
// ✅ declare 才是正确做法
declare function myFunc(a: number): number;
场景 2:给对象加类型
// 两者都可以,但用途不同
// interface:定义对象类型(主要用途)
interface User {
name: string;
}
// declare:告诉 TS 这个全局对象存在
declare const currentUser: { name: string };
// 或者结合两者
declare const currentUser: User;
.d.ts 常见用法场景
场景 1:为现有 JavaScript 库添加类型
// math.d.ts
export function add(a: number, b: number): number;
export function multiply(a: number, b: number): number;
场景 2:定义全局类型
// global.d.ts
declare global {
interface Window {
myCustomAPI: {
getData(): Promise<any>;
};
}
}
场景 3:声明模块
// types/custom-module.d.ts
declare module 'custom-module' {
export function doSomething(): void;
}
DefinitelyTyped 与 @types
关系说明
1. DefinitelyTyped 仓库
- GitHub 上的主仓库:https://github.com/DefinitelyTyped/DefinitelyTyped
- 社区维护的大型项目
- 包含成千上万个 JavaScript 库的类型声明
2. @types 包
- 是从 DefinitelyTyped 自动生成和发布到 npm 的
- 例如
@types/react对应 DefinitelyTyped 仓库中的types/react目录 - 通过自动化流程定期同步更新
工作流程
DefinitelyTyped 仓库(源代码)
↓
自动发布脚本
↓
npm registry 上的 @types/* 包
↓
npm install @types/xxx
↓
你的项目
实际使用
当运行 npm install @types/react 时,实际上安装的是:
- DefinitelyTyped 仓库中
types/react/目录下的内容 - 被打包成一个 npm 包发布到 npm registry
如果想为某个库贡献类型声明,通常就是向 DefinitelyTyped 仓库提 PR。
编译时 vs 运行时
编译和运行的分离
这是 TypeScript 最重要的特性之一: .d.ts 文件只在 TypeScript 编译时使用,编译后会被完全丢弃。
具体流程
┌─────────────────────────────────────┐
│ 开发时:有 .ts 和 .d.ts │
│ TypeScript 用 .d.ts 做类型检查 │
└─────────────────────────────────────┘
↓ 编译
┌─────────────────────────────────────┐
│ 编译后:只有 .js │
│ 运行时只需要对应包的 .js 代码 │
│ .d.ts 文件被完全丢弃 │
└─────────────────────────────────────┘
具体例子
TypeScript 源代码
// src/index.ts
import React from 'react';
const app = <div>Hello</div>;
编译后的 JavaScript
// dist/index.js
const React = require('react');
const app = React.createElement('div', null, 'Hello');
运行时过程
1. 编译时:
- TypeScript 编译器查找
@types/react的.d.ts文件 - 验证
React.createElement的参数类型是否正确 - 编译为 JavaScript,
.d.ts文件被丢弃
2. 运行时:
- Node.js 或浏览器加载编译后的
.js文件 - 执行
require('react')加载实际的 JavaScript 包 - 此时根本不需要
.d.ts文件了 - 运行
React.createElement函数的真实实现
关键认识
- .d.ts 文件只在 TypeScript 编译时使用(用来验证类型)
- 编译后 .d.ts 被丢弃,只剩 .js 文件
- 运行时依赖的是对应包的 JavaScript 代码
- .d.ts 和 .js 是平行的关系:
.d.ts提供类型信息,.js提供实现
所以不用担心——.d.ts 就像一个"类型助手",只在开发编译阶段出现,运行时完全靠真实的 JavaScript 包。
总结
1. .d.ts 文件是约定名称,本质上就是 TypeScript 代码,只是用来声明类型而不包含实现
2. declare 关键字的作用是告诉编译器"这个东西存在",允许只声明类型而不提供实现
3. declare 和 interface 不能互相替代:
interface只能定义对象结构declare可以声明任何东西(函数、变量、类、模块等)
4. @types 生态由 DefinitelyTyped 社区维护,为 JavaScript 库提供类型支持
5. 编译时和运行时的分离是 TypeScript 的核心特性:
- 编译时:
.d.ts提供类型检查 - 运行时:
.d.ts被丢弃,只有.js代码执行
q