TypeScript declare 关键字作用详解(详解 .d.ts 与环境声明实战)

declare 是 TypeScript 用来告诉编译器”这个东西在运行时已经存在,你别再要求我写实现”的标记,它本身不产生任何 JavaScript 代码,编译时会被完整擦除。常见落地形态是 .d.ts 声明文件、declare module、declare namespace 与 declare global 四件套,覆盖了第三方无类型包、浏览器全局对象、Node 环境扩展与全局变量增强等几乎所有”在 TS 里使用非 TS 源码”的场景。

一、declare 的本质:只描述类型,不产生运行时代码

declare 修饰的所有声明都属于”环境声明(ambient declaration)”——告诉编译器某个变量、函数、类、命名空间或模块在别处已经存在,TS 只负责类型校验与提示,不负责实现。任何在 declare 上下文中写函数体、类方法实现或变量赋值,都会触发 “An implementation cannot be declared in ambient contexts” 报错。

// ❌ 不允许:declare 上下文中不能有实现
declare function sum(a: number, b: number): number {
  return a + b;
}

// ✅ 正确:只声明签名
declare function sum(a: number, b: number): number;

这种”只承诺存在、不给出实现”的语义,是 TS 与原生 JS 生态互通的桥梁——你能在不重写老 JS 代码的前提下给它补齐类型。

二、四大常见用法速览

按使用频率从高到低排列,四种形态覆盖了日常 90% 以上的环境声明需求。

用法 形态 典型场景
全局变量声明 declare const/let/var 浏览器 script 标签注入的 $、jQuery
全局函数/类 declare function/class UMD 库挂到 window 上的工具方法
模块声明 declare module 'name' 没有自带类型的 npm 包、CommonJS 库
全局增强 declare namespace / declare global 扩展 Window、NodeJS.ProcessEnv、JSX

如果只是引用第三方包且包没自带类型,首选一定是去 npm install @types/xxx;只有 @types 不存在或与业务类型不一致时,再手写 .d.ts。

三、.d.ts 声明文件怎么组织

.d.ts 文件只放类型描述,不放运行时代码,编译后不会出现在产物里。它相当于 TS 世界的”头文件”,告诉编译器”这玩意长啥样”。tsconfig.json 中 include、typeRoots、types 三个字段决定了哪些 .d.ts 会被自动加载。

// tsconfig.json
{
  "compilerOptions": {
    "typeRoots": ["./types", "./node_modules/@types"]
  },
  "include": ["src/**/*", "types/**/*"]
}

新建 types/jquery.d.ts,把第三方 jQuery 的全局 $ 补成可用类型:

// types/jquery.d.ts
declare const $: (selector: string) => {
  show(): void;
  hide(): void;
  on(event: string, handler: (...args: any[]) => void): void;
};

之后在业务代码里直接用 $(...) 不会再报”找不到名称”。

四、declare module:给无类型 npm 包打补丁

项目里经常遇到 npm 拉下来的包没有 .d.ts,也没 @types/xxx 维护(比如某些内部包、老古董 CommonJS 模块)。这时用 declare module '包名' 给它一个最小可用类型壳。

// types/custom-lib.d.ts
declare module 'custom-lib' {
  export function doSomething(input: string): number;
  export const version: string;
  export interface Options {
    debug?: boolean;
    timeout?: number;
  }
  export default function init(options?: Options): void;
}
// 业务代码
import init, { doSomething, version } from 'custom-lib';
init({ debug: true });

如果是更轻量的”先让 import 不报错就行”,可以省略内部签名,所有导入值退化为 any:

declare module 'untyped-legacy-pkg';

一个高频踩坑点:declare module 'x' 在没先 import 过同名模块时,会整体替换该模块的现有类型;如果只是想扩展类型,记得先写一行 import type * as X from 'x'; 再 declare 才会走”声明合并”分支。

五、declare namespace 与 declare global:扩展全局对象

declare namespace 适合描述”通过 script 标签加载的全局工具库”——比如 Google Maps SDK、Stripe.js、Analytics 脚本,调用时不需要 import,类型也得能在任意位置被识别。

// types/google-maps.d.ts
declare namespace googleMaps {
  function initMap(containerId: string, lat: number, lng: number): void;
  interface LatLngLiteral {
    lat: number;
    lng: number;
  }
}
googleMaps.initMap('map', 39.9, 116.4);

declare global 则用于在”已经是模块的 .d.ts 文件里”扩展全局对象,常见于扩展浏览器 Window、自定义 JSX 元素、NodeJS.ProcessEnv 等。

// types/globals.d.ts
export {};

declare global {
  interface Window {
    __APP_VERSION__: string;
    analytics: (event: string, payload?: Record<string, unknown>) => void;
  }
}

注意文件首行 export {}——它把文件从”脚本”切换到”模块”模式,否则 declare global 不会生效,这是日常最容易写错的一处。

六、给环境变量补类型

Node/前端项目里 process.env.NODE_ENV、import.meta.env.VITE_API_BASE 这些变量 TS 默认认不出,必须显式声明。

// types/env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: 'development' | 'production' | 'test';
    DATABASE_URL: string;
    API_SECRET: string;
  }
}

补完之后,编辑器会在 process.env 上自动补全、给出类型校验。注意这是编译期类型,运行时变量是否真存在还是要靠启动时校验库(如 zod、envalid)兜底。

七、模块声明合并 vs 替换:常被忽略的关键差异

declare module 'x' 看起来是”加一段类型”,但实际行为取决于该模块是否已经被 TS 识别。若同名模块尚未被 import 过,TS 会把整张类型表替换为你的声明——包原本自带的类型也会被一起丢掉,后续用到的所有类型都退化成你声明的形状。只有先 import type 或 import 一下该模块,再写 declare module,TS 才会走”声明合并”路径,把新增成员并入原表。这个差别在 monorepo 与第三方包扩展场景里非常容易踩到,遇到”明明装了 @types 却报一堆找不到属性”时,第一反应就应该是检查这个合并/替换分支。

八、避坑清单

新手用 declare 经常踩的几个坑,整理成检查表:

  1. 不要在 .ts 业务文件里到处写 declare——只在你”不拥有实现”的地方用,自己写的 TS 函数直接写实现就行。
  2. 不要在 declare 里写默认值——declare function foo(x: number = 0) 会报错,默认值是实现层的事。
  3. declare 不会出现在 JS 产物里——它只是类型层的承诺,别指望它”运行”任何东西。
  4. 模块声明前先 import 一次——否则会把包自带类型整个顶替掉。
  5. skipLibCheck: true 会静默吞掉 .d.ts 错误——配置时心里要有数,本地类型出错时记得临时关掉排错。
  6. 优先用 @types 包——只有社区没维护时才自己写 .d.ts,免得长期维护成本高。

常见问题(FAQ)

Q1:能不能在 .ts 业务文件里直接写 declare module?

能,但只对当前文件所在作用域生效;跨文件复用还是要放进 .d.ts 单独维护。

Q2:为什么 @types/xxx 已经装了却报错找不到类型?

常见原因有三种:tsconfig typeRoots 没覆盖 node_modules/@types、版本对不上、或者包自带类型与 @types 冲突,需要查重。

Q3:什么情况下 declare 声明会被合并,什么情况下会被替换?

未先 import 同名模块时整体替换;先 import 再 declare 走声明合并,全局接口(如 Window)天然合并。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
小码农的头像小码农认证作者

相关推荐

返回顶部