模块管理
模块
TS
支持JS
的所有模块化方案,TS
使用了ES Module
作为默认的模块化方案。
在TS
文件中没有添加import
或export
,都会被认为是一个全局脚本而不是模块。被认为是脚本内容是全局可见的,容易造成全局变量污染、变量名冲突等问题。
类型导入
在导入类型时,明确标注导入的内容是TS
类型,目的是让非TS
编译器比如babel
、swc
在编译的时候跳过语法检查,并安全移除类型。
import { type IProps } from './typings/module'
import type { IProps, IPerson } from './typings/module'
declare
declare
在TS
中用于声明一个变量、类型、函数或模块的存在,不需要提供具体的实现,声明的变量尽量放在.d.ts
文件中,做到实现和类型分离。
声明全局变量、函数、类型
// global.d.ts
declare const APP_ENV: 'development' | 'production';
declare function trackEvent(eventName: string): void;
// 使用
console.log(APP_ENV); // 类型安全检测
trackEvent('page_load'); // ✅ 正确
trackEvent(123); // ❌ 类型错误
为第三方模块声明类型
// jquery.d.ts
declare module 'jquery' {
export function ajax(url: string): Promise<any>;
export const version: string;
}
// 使用
import { ajax } from 'jquery';
ajax('/api/data'); // 获得类型检查
扩展已有类型
// 扩展 Window 对象
declare global {
interface Window {
_gaq: any[];
}
}
// 使用
window._gaq.push(['trackPageview']); // 不再报错
声明模块(非JS
资源)
// images.d.ts
declare module '*.png' {
const src: string;
export default src;
}
// 使用
import logo from './logo.png'; // 类型为 string
类型声明文件.d.ts
.d.ts
是用来做类型声明的文件,仅仅用来做类型检查,告诉TS
存在的类型,它不存在任何JS
运行时代码的实现,声明的内容默认是全局可见的。
// type.d.ts
type IProps = {};
// 使用,无需导入
const props: IProps = {};
编译器在查找类型时,会根据就近原则,会在以下的文件进行查找:
- 内置类型声明:
TS
自带类型库,内置了JS
运行时标准API
的声明文件。 - 第三方包类型声明:第三方包编写的类型文件,一般在包的构建产物就带有,或者在
/node_modules/@types/xxx
目录下。 - 自定义类型声明:有的第三方包是
JS
编写的可能会存在没有类型文件的情况,开发者可以自己编写类型声明文件,为第三方包提供类型。
命名空间
命名空间是早期TS
用于解决全局作用域污染的方案,但现代的模块化体系已经能很好处理作用域隔离的问题,所以命名空间渐渐退出舞台。
namespace
相当于一个独立的作用域,可以在里面正常编写TS
代码,比如下面的代码:
namespace Utils {
export function getRandomNum() {
return Math.random();
}
}
const randomNum = Utils.getRandomNum();
在TS
文件中使用namespace
最终会被编译成JS
代码,就像下面的结果:
var Utils;
(function (Utils) {
function getRandomNum() {
return Math.random();
}
Utils.getRandomNum = getRandomNum;
})(Utils || (Utils = {}));
var randomNum = Utils.getRandomNum();
在模块化的项目中,优先使用ES Module
;在非模块化的全局脚本中,命名空间仍然有用。
类型合并
// 合并分散的类型定义
// user.d.ts
namespace API {
export interface User { id: number; }
}
// product.d.ts
namespace API {
export interface Product { sku: string; } // 合并到同一命名空间
}
全局类型扩展
搭配declare
使用,形成全局类型。
// 扩展第三方库类型
declare namespace React {
interface FunctionComponent {
displayName?: string;
}
}
兼容全局脚本
在兼容以CDN
或者没有类型的全局脚本的场景,namespace
是很有用处的。
<!-- 传统脚本引入 -->
<script src="jquery"></script>
// jquery.d.ts
declare module "jquery" {
export function ajax(url: string): Promise<any>;
export const version: string;
}
三斜线指令
/// <reference>
语法是一种特殊的TS
注释,用于编译时告诉编译器在编译过程中要引入额外的类型文件。
声明语句通常出现在文件顶部,如果出现在一个语句或声明之后,会被视为普通的单行注释,不会有任何特殊含义。
引入类型文件
它可以引入本地路径的类型文件(.d.ts
)、@types
的类型包、内置的lib
类型声明(ES2015
、dom
等)
- 路径引入
// 相对路径
/// <reference path="./types/index.d.ts" />
// 绝对路径
/// <reference path="D:/project/types/index.d.ts" />
@types
类型包
// @types的类型包
/// <reference types="node" />
- 内置的
lib
类型
// 内置lib类型
/// <reference lib="es2015" />
编译选项
当使用 /// <reference>
时,需要确保 TypeScript 编译器知道如何处理这些指令。可以通过以下编译选项来配置:
--noResolve
:禁用自动解析模块,编译器不会自动解析/// <reference>
中的文件--noLib
:禁用所有默认标准库文件,需要手动指定/// <reference>
// 在 TypeScript 文件顶部手动引入 ES2015 标准库
/// <reference lib="es2015" />
WARNING
它的处境跟命名空间是一样的,在现代的模块化体系项目中,优先使用模块import/export
的方式导入导出。如果在没有模块系统的环境中开发,或者需要处理全局变量、全局类型时,/// <reference>
会派上用场。
最佳实践
场景 | 推荐方案 |
---|---|
第三方库类型扩展 | **declare module** |
项目自有类型 | 模块化导出 (**export** ) |
全局环境变量 | **global.d.ts** +**declare** |
复杂类型组织 | 命名空间嵌套 |
类型复用 | 集中导出 (**index.ts** ) |
非 JS 资源导入 | 模块声明 (***.d.ts** ) |
最佳实践
- 扩展第三方包
declare module "vue" {
interface ComponentCustomProperties {
$translate: (key: string) => string;
}
}
// 如果没有export 将会覆盖vue的全部类型声明,而不是扩展它
export {}
- 扩展文件资源模块
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
- 扩展全局属性
declare global {
interface Window {
_gaq: any[];
}
}
- 请求:整合类型
对于网络请求的请求体DTO
和响应体Resp
要抽离成类型声明文件,建议整合类型,并且声明命名空间API
。
// camera.d.ts
declare namespace API{
interface CameraDto{}
}
// base-device.d.ts
declare namespace API{
interface BaseDeviceResp{}
}