想象你从海外买回一台全英文界面的电饭煲。你虽然知道它能煮饭,却分不清哪个按钮对应“预约”,哪个对应“保温”。只要在机身贴一张中文说明贴纸,把每个按钮的功能写得清清楚楚,你就能放心操作了。 JavaScript 库对 TypeScript 编译器而言,就是这台“全英文电饭煲”。它本身能正常运行,但缺少一份让编译器读得懂的类型说明。声明文件(.d.ts)正是这样的“贴纸”:它不提供实现,只把库里的变量、函数、类、模块的形状描述出来。编译器读完声明文件,就掌握了所有按钮的用途——你写错参数时它会立刻提醒,编辑器也会据此给出智能补全。 这篇文章将带你从零学会编写声明文件:先理解它存在的意义,再学习基础语法与各种声明形式,深入 ambient module、三斜线指令,最后掌握如何把声明随库发布并长期维护。全程用“贴纸说明书”这条主线贯穿,让每个术语都变得直观可感。

岗位关联

在 AI 领域,你常要接入开源 JS 工具库或自研前端包;会写声明文件,意味着类型安全、编辑器提示和团队协作效率都能上一大步。

目录
  1. 可视化演示
  2. 为什么需要声明文件
  3. 声明文件基础:.d.ts 的书写规矩
  4. 声明语句:变量、函数、类与命名空间
  5. 环境模块:为“不存在的文件”建立类型入口
  6. 三斜线指令:声明文件之间的“交叉引用”
  7. 配置与发布:让“类型贴纸”随包交付
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

演示.d.ts声明文件结构、JS到TS类型映射流程及ambient模块声明机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

声明文件结构 declare module定义模块类型 interface/type/fn声明 .d.ts=declare{interface,type,function} 声明文件为JS库提供TypeScript类型定义

为什么需要声明文件

想象你从海外代购了一台咖啡机。机器浑身都是英文按钮,你不敢乱按,生怕把某个功能键按坏。这时机身贴了一张中文贴纸:“左键=美式,右键=拿铁,中键=清洗”。从此,你闭着眼睛都不会按错。JavaScript 库就是这样一台咖啡机:它本身运行良好,但对 TypeScript 编译器来说,它只是“一盒按钮”,没有人告诉编译器这些按钮分别有什么作用。声明文件(.d.ts)就是那张中文贴纸。

没有声明文件时,你在 TypeScript 里 import 一个纯 JS 库,编译器要么报“无法找到模块的声明文件”,要么只能把这个库当作 any 类型处理。any 等于放弃了所有检查:参数可以随便传,属性可以随便写,直到运行时才猛然报错。有了 .d.ts 声明,原本模糊的库就变成了一张清晰的接口表:每个函数该传什么参数、返回什么值、类的实例有哪些方法,编译器在保存代码的那一刻就能帮你盯住。

这层“贴纸”的价值还体现在开发体验上:编辑器根据声明文件提供智能补全、跳转定义和重命名重构。团队协作时,别人阅读你的代码,也能通过声明文件准确理解每个 API 的意图。社区甚至孕育了庞大的 DefinitelyTyped 仓库,为数千个流行库统一维护声明文件,并以 @types/* 包的形式发布。

回到那个黑盒:没有贴纸时,你只能拆开机器猜按钮;贴好声明文件后,编译器不仅看懂每个按钮,还会在你按错时大声提醒。声明文件,就是现代前端工程里的“防呆贴纸”。

TypeCheck(C, D)={✓若每个 c∈C 都与 D 中的声明匹配错误清单否则\text{TypeCheck}(C,\,D) =\begin{cases}\checkmark & \text{若每个 } c \in C \text{ 都与 } D \text{ 中的声明匹配}\\\text{错误清单} & \text{否则}\end{cases}
这个公式把“类型检查”抽象成一次双向核对:左边是项目代码的全部使用行为,右边是声明文件给出的类型说明,两者对不上就输出具体错误。
  1. 输入代码集项目里所有真实发生的调用,例如 add(1, 2)、sayHello("AI"),这些构成了集合 $C$。
    C={ c1, c2, …, cm }C = \{\, c_1,\, c_2,\, \dots,\, c_m \,\}
  2. 声明集合声明文件组成一张“形状字典” $D$,记录每个名字对应的类型签名。
    D={ d1, d2, …, dn }D = \{\, d_1,\, d_2,\, \dots,\, d_n \,\}
  3. 逐一匹配每次调用都要找到对应声明并比对参数、返回值是否兼容,遇到不匹配就记录下来。
    ∀ c∈C, ∃ d∈D:调用(c)∼签名(d)\forall\ c \in C,\ \exists\ d \in D : \text{调用}(c) \sim \text{签名}(d)
  4. 输出结论全部匹配时返回 $\checkmark$,否则返回错误清单,例如“string 不能赋给 number”。
    错误清单={ e1, e2, …, ek }\text{错误清单} = \{\, e_1,\, e_2,\, \dots,\, e_k \,\}

声明文件基础:.d.ts 的书写规矩

贴纸不能随手乱写。如果一张说明贴纸字迹潦草、布局混乱,用户照样会按错。.d.ts 文件也有一套清晰的结构规则:它只描述类型、不包含实现,就像一张“纯排版”的标签,禁止出现函数体或变量赋值。

.d.ts 文件被编译器视为“环境声明”(ambient declaration)。文件内常见的结构是:顶部可写三斜线指令,中间是若干 declare 语句,模块化的声明文件还可以用 export 导出类型。一个 .d.ts 文件如果没有 import 或 export,它就是一个全局脚本,里面的声明自动作用于全局;一旦出现 import 或 export,文件就变成模块,声明变为模块局部,需要显式导出才能被外部引用。这个边界决定了你的“贴纸”是贴在全局展板上,还是贴在某个专属角落。

关键词 declare 是整张贴纸的“胶水”。它告诉编译器:“我描述的这个名字在运行时确实存在,但实现不在当前文件里,你只需要记住它的类型。”例如 declare const version: string; 表示存在一个名为 version 的全局字符串常量。在模块化的声明文件中,你还可以用 export const version: string 的形式,把类型信息作为模块导出的一部分。

换句话说,.d.ts 不是写给浏览器运行的代码,而是写给编译器的“排版说明书”。它唯一的工作,就是把每个名字的类型挂在正确的位置上,让 TypeScript 在编译期间有据可查。

Γ⊢x:T⟵declare const x:T\Gamma \vdash x : T \quad \longleftarrow \quad \texttt{declare const } x: T
“类型环境” $\Gamma$ 就好比贴纸墙:每贴一条声明,墙上就多一行记录。编译器在检查代码时,只要查墙上有 $x:T$,就按这个类型来校验。
  1. 声明登记declare const x: T 里,编译器把名字 $x$ 和类型 $T$ 记入类型环境。
    Γ′=Γ∪{ x:T }\Gamma' = \Gamma \cup \{\, x: T \,\}
  2. 推导规则$\vdash$ 表示“可以推出”,即“在类型环境中查得到”。
    Γ⊢x:T⇔x:T∈Γ\Gamma \vdash x : T \quad \Leftrightarrow \quad x: T \in \Gamma
  3. 消费生效后续任意代码使用 $x$ 时,都会按照 $T$ 来检查赋值、运算和传参。
    const y: T = x;⇒合法\texttt{const y: T = x;} \quad \Rightarrow \quad \text{合法}

声明语句:变量、函数、类与命名空间

一份说明书会区分“功能键”“模式选择”“状态灯”等不同条目。同理,声明文件按实体的性质分成几类写法:变量用 declare var/let/const,函数用 declare function,类用 declare class,一批相关声明可以收进 declare namespace 或 declare enum。

它们是声明文件的基本积木。以变量为例:

函数声明只写签名,不带函数体:

类声明则描述构造器和成员方法:

当某个库包含一系列相关工具时,可以用命名空间把声明组织起来,例如 declare namespace Utils { const PI: number; function format(n: number): string; }。命名空间像说明书里的小节标题,把零散条目归类,避免污染全局命名空间。

回到电饭煲说明书:变量是“当前温度显示”,函数是“开始煮饭”按钮,类是“整机模式”,命名空间则是“高级设置”这一整页。每类条目用对应的语法书写,编译器才能准确地把你的操作映射到正确的位置上。

f:A×B→R,f(a, b) ⇒ 返回值类型 Rf: A \times B \to R, \qquad f(a,\, b)\ \Rightarrow\ \text{返回值类型 } R
函数声明本质上描述一条“输入→输出”的类型通道。参数类型列表构成通道入口,返回类型构成出口。
  1. 参数字段$A$、$B$ 分别对应函数签名里第一个、第二个参数的类型。
    add(a:number, b:number)  ⇒  A=B=number\texttt{add}(a: \text{number},\, b: \text{number}) \;\Rightarrow\; A = B = \text{number}
  2. 返回字段签名中冒号后面的类型 $R$ 是返回值类型。
    add:number×number→number\texttt{add}: \text{number} \times \text{number} \to \text{number}
  3. 调用检查实参类型与 $(A,B)$ 必须兼容,否则编译报错。
    add("1", 2)  ⇒  “string 不能赋给 number”\texttt{add}("1",\, 2) \;\Rightarrow\; \text{“string 不能赋给 number”}
ts3 行
declare var version: string;declare let counter: number;declare const MAX_SIZE: number;
ts2 行
declare function add(a: number, b: number): number;declare function greet(name: string): string;
ts5 行
declare class Widget {  constructor(id: string);  render(): void;  setTitle(title: string): void;}

环境模块:为“不存在的文件”建立类型入口

有些库没有配套的 TypeScript 实现文件,但你依然想让 import 语句能够解析。这时就需要“环境模块”(ambient module)——它相当于在目录索引里为某个名字专门开辟一个章节:虽然物理上找不到这个模块文件,但类型层面已经为它设计好了完整入口。

写法是 declare module "模块名" { ... },模块名是字符串字面量,不需要真实文件路径与之对应。例如:

之后项目中任意 import { runPrompt } from "my-ai-sdk" 都能获得完整类型提示。另一种常见场景是处理非代码资源:图片、样式、模型文件等。TypeScript 默认无法识别 import logo from "logo.png",此时可以声明通配符模块:

通配符 * 匹配任意以 .png 结尾的导入路径,相当于在目录索引里统一写了一条规则:凡是以这个后缀结尾的资源,都按“导出默认字符串路径”来理解。这也是 AI 前端项目中加载模型文件、图片素材时最常见的声明套路。

环境模块让“目录索引”变得极其灵活:既可以精确指定某个库名,也可以用通配符成片覆盖资源模块。它解决的是“文件不在,但类型必须在”的尴尬,让那些没有类型的 JS 世界与 TypeScript 世界顺畅握手。

name  ↦ 声明   Shape(S),S={ runPrompt, version, … }\text{name} \;\xmapsto{\ \text{声明}\ }\; \text{Shape}(S), \qquad S = \{\, \text{runPrompt},\ \text{version},\ \dots \,\}
环境模块的实质是把“模块名”映射到“一组导出的类型形状”。只要名字对得上,编译器就按这份形状来检查导入。
  1. 模块名字符串字面量 "my-ai-sdk" 是索引里的条目名,编译器和导出方都通过它定位。
    name="my-ai-sdk"\text{name} = \texttt{"my-ai-sdk"}
  2. 导出形状花括号内的 export function、export const 组成形状集合 $S$。
    S={ e1, e2, …, en }S = \{\, e_1,\, e_2,\, \dots,\, e_n \,\}
  3. 映射关系模块解析时查这张表,命中便获得对应类型。
    import "my-ai-sdk"  ⇒  Shape(S)\texttt{import "my-ai-sdk"} \;\Rightarrow\; \text{Shape}(S)
  4. 消费匹配若导入的名字不在 $S$ 中,编译器报“没有导出该成员”。
    import { nope }  ⇒  错误\texttt{import \{ nope \}} \;\Rightarrow\; \text{错误}
ts4 行
declare module "my-ai-sdk" {  export function runPrompt(prompt: string, options?: { temperature: number }): Promise<string>;  export const version: string;}
ts4 行
declare module "*.png" {  const url: string;  export default url;}

三斜线指令:声明文件之间的“交叉引用”

厚厚的手册里,某一页常常写着“详见第 42 页”。三斜线指令就是声明文件中的这种交叉引用。它以 /// 开头,看起来像注释,却会被 TypeScript 编译器在预处理阶段解析,用来声明“这个文件依赖哪些其他类型文件”。

三种最常用的指令分别是:/// <reference types="node" /> 表示依赖 @types/node 提供的全局类型;/// <reference path="./jquery.d.ts" /> 表示按相对路径引入本地声明文件;/// <reference lib="dom" /> 表示引用 TypeScript 内置的 lib.dom.d.ts。它们都必须写在文件最顶部,且不能与其他普通注释混杂,否则编译器可能忽略。

在实际工程中,三斜线指令的“使用频率”已经下降,因为现代编译器更推荐通过 tsconfig.json 的 include 和 types 字段统一管理文件加载。但在旧代码库、手写声明文件或某些需要在声明内部单独指定依赖的场景里,它依然是兜底的利器。理解它,就是理解“贴纸之间的黏连关系”:

这就像你在手册里写“清洗功能详见第 42 页”,读者翻过去就能看到完整说明。三斜线指令保证声明文件之间可以相互引用而不必重复粘贴,让整本“说明书”既精简又完整。

G=(V, E),e=(d1, d2)∈E  ⟺  d1 引用了 d2G = (V,\, E), \qquad e = (d_1,\, d_2) \in E \iff d_1 \text{ 引用了 } d_2
三斜线引用把所有声明文件织成一张依赖图。编译器按照依赖顺序加载,先加载被引用的文件,再加载主动引用的文件。
  1. 顶点集合每个顶点 $d \in V$ 代表一个声明文件。
    V={ d1, d2, …, dn }V = \{\, d_1,\, d_2,\, \dots,\, d_n \,\}
  2. 边集合一条边 $(d_1, d_2)$ 表示 $d_1$ 通过三斜线指令引用了 $d_2$。
    E⊆V×VE \subseteq V \times V
  3. 拓扑加载编译器按依赖顺序处理,被引用的声明先进入类型环境。
    load(d2)≺load(d1)if (d1,d2)∈E\text{load}(d_2) \prec \text{load}(d_1) \quad \text{if } (d_1, d_2) \in E
  4. 类型可见性$d_2$ 中声明的类型在 $d_1$ 中直接可用。
    T∈Types(d2)⇒T∈Types(d1)T \in \text{Types}(d_2) \Rightarrow T \in \text{Types}(d_1)

配置与发布:让“类型贴纸”随包交付

贴纸做好之后,还要让它被正确贴上、随商品一起上架。对声明文件而言,这涉及两个环节:本地如何被编译器加载,发布后如何被消费方自动识别。

本地配置主要看 tsconfig.json。include 字段决定编译器扫描哪些文件,通常包含 src//*.ts 和 types//*.d.ts;types 字段限制自动加载的 @types/* 包,例如 "types": ["node", "jest"];更进阶的 typeRoots 可以自定义类型包搜索目录。发布环节则依赖 package.json 的 types(或 typings)字段:

对于没有自带类型声明的流行库,社区会维护对应的 @types/* 包,例如 @types/jquery 对应 jquery,版本号保持同步。无论是随包发布还是通过 @types 分发,核心目标都是:当别人安装你的库时,TypeScript 能自动找到那份“标签纸”,无需手动配置。

发布之前,记得用 tsc --noEmit 检查声明文件自身的语法,用 tsd 之类的工具做类型级测试,确保“标签描述”和“真实商品”一致。否则就会出现最尴尬的情况:编译时类型说有条“煮饭”按钮,运行时按下去却是“保温”。

把声明文件当作商品标签来管理:本地配置决定标签贴在货架的哪个位置,发布字段决定顾客能不能一眼找到,而一致性验证保证标签上的每一个字都真实可信。这样,你的库在 TypeScript 的世界里便是一位“自带说明书的靠谱供应商”。

types:包名⟼路径(.d.ts),T声明≡T实现\text{types}: \text{包名} \longmapsto \text{路径}(.d.ts), \qquad T_{\text{声明}} \equiv T_{\text{实现}}
发布声明文件的关键就两条:入口地址要正确,声明形状要与实现等价。地址错了找不到贴纸,形状错了贴纸会误导人。
  1. 入口映射package.json 的 types 字段把包名映射到主声明文件路径。
    "my-lib"⟼"./dist/index.d.ts"\texttt{"my-lib"} \longmapsto \texttt{"./dist/index.d.ts"}
  2. 消费端加载安装方 TypeScript 通过该字段自动读取声明,无需再手动 reference。
    消费方编译⇒加载 types 路径\text{消费方编译} \Rightarrow \text{加载 types 路径}
  3. 一致性约束声明中的类型 $T_{\text{声明}}$ 必须与真实实现等价 $T_{\text{实现}}$。
    T声明≡T实现  ⇒  无运行时意外T_{\text{声明}} \equiv T_{\text{实现}} \;\Rightarrow\; \text{无运行时意外}
  4. 验证手段tsc --noEmit 与类型测试确保发布前声明语法正确。
    Validation(D)=✓  ⇒  可发布\text{Validation}(D) = \checkmark \;\Rightarrow\; \text{可发布}
json5 行
{  "name": "my-lib",  "main": "./dist/index.js",  "types": "./dist/index.d.ts"}

AI 应用场景

为自研 AI 推理 SDK 补齐类型声明

阶段:AI 应用前端接入 / SDK 封装

解决的问题:团队封装大模型网关 SDK 后,下游同学在 TypeScript 项目中调用时常写错参数名、把 temperature 写成字符串、漏传 required 字段,运行时才发现问题。为 SDK 编写 .d.ts 声明后,编辑器能立刻提示所有参数与返回类型。

具体案例:某 AI 中台团队开发的 ai-gateway 包,内部用纯 JavaScript 封装了 chat、embedding、rerank 等接口。前端组在 types 目录下编写 ai-gateway.d.ts,用 declare module "ai-gateway" 描述每个方法,并补充 Promise<Stream<Chunk>> 等复杂返回类型。此后所有调用方都享受到智能补全与编译期校验,联调事故率显著下降。

用通配符声明加载模型与资源文件

阶段:Web 端模型部署 / 资源管理

解决的问题:浏览器端运行 TensorFlow.js 或 ONNX Runtime Web 时,import modelUrl from "./model.onnx" 会让 TypeScript 报错“找不到模块声明”。这种非代码资源没有类型信息,恰好是通配符模块的用武之地。

具体案例:某智能问答前端项目在 global.d.ts 中声明 declare module "*.onnx" 和 declare module "*.bin",并导出默认字符串路径。模型文件、分词器文件都能被安全引入和打包,类型检查全程绿灯。

为开源可视化库补充声明文件

阶段:数据可视化 / 结果呈现

解决的问题:老版本 D3、Leaflet 等可视化库常缺少 TypeScript 类型,AI 项目需要在大屏上绘制图表与地图,但 API 全靠猜。手写声明文件或向 DefinitelyTyped 提交 @types/* 包,可以让团队稳定复用。

具体案例:某 AI 产品团队为内部地图组件编写 leaflet-extra.d.ts,用 declare class 和 declare namespace 描述图层、标记、弹窗等接口,再通过 tsconfig include 纳入项目。地图模块从“运行时错误不断”变成“编译期全量检查”。

Python 演示

python69 行
"""迷你声明文件模拟器:解析 .d.ts 中函数签名并模拟基础类型检查""" import re  def parse_declarations(dts_text):    """从 .d.ts 文本中提取 declare function 签名"""    decls = {}    pattern = re.compile(        r"declare\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*(\w+)\s*;"    )    for match in pattern.finditer(dts_text):        fn_name, params, ret = match.group(1), match.group(2), match.group(3)        param_types = []        for param in params.split(','):            param = param.strip()            if ':' in param:                param_types.append(param.split(':')[-1].strip())            else:                param_types.append('any')        decls[fn_name] = (param_types, ret)    return decls  def infer_literal_type(literal):    """根据字面量文本推断基础类型"""    if re.fullmatch(r'[+-]?\d+', literal):        return 'number'    if re.fullmatch(r'"(?:[^"\\]|\\.)*"', literal):        return 'string'    if literal in ('true', 'false'):        return 'boolean'    return 'unknown'  def simulate_type_check(dts_text, code_text):    """模拟 TypeScript 编译器核对调用与声明"""    decls = parse_declarations(dts_text)    results = []    call_pattern = re.compile(r"(\w+)\s*\(([^)]*)\)\s*;")    for match in call_pattern.finditer(code_text):        fn_name, args = match.group(1), match.group(2)        if fn_name not in decls:            results.append((fn_name, args, 'MISSING_DECL'))            continue        param_types, _ = decls[fn_name]        arg_literals = [a.strip() for a in args.split(',') if a.strip()]        arg_types = [infer_literal_type(a) for a in arg_literals]        if arg_types != param_types and 'any' not in param_types:            results.append((fn_name, args, 'TYPE_MISMATCH'))        else:            results.append((fn_name, args, 'PASS'))    return results  if __name__ == '__main__':    dts = """    declare function add(a: number, b: number): number;    declare function greet(name: string): string;    declare function log(value: any): void;    """    code = """    add(1, 2);    add("1", 2);    greet("AI");    sendData(42);    """    for fn_name, args, status in simulate_type_check(dts, code):        print(f"{fn_name}({args or ''}): {status}")
目的:用 Python 模拟 TypeScript 编译器对 .d.ts 声明文件的最小类型检查流程,帮助理解声明文件是如何参与编译期校验的。
  1. pattern = re.compile(r"declare\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*(\w+)\s*;")用正则从声明文件中抽取函数名、参数列表和返回类型。
  2. param_types.append(param.split(':')[-1].strip())从单个参数片段 a: number 中取出类型名 number。
  3. infer_literal_type()根据调用代码里的字面量推断类型,1 推断为 number,"AI" 推断为 string。
  4. arg_types != param_types and 'any' not in param_types若实参类型列表与声明的形参类型不一致,且声明没有使用 any 兜底,则判定为类型错误。
  5. sendData(42)调用了一个在声明文件中不存在的函数,因此返回 MISSING_DECL,对应编译器“找不到声明”的场景。

常见误区

误区学了 `.ts` 之后,我觉得 `.d.ts` 就是把实现代码直接抄一遍,反正 TypeScript 能识别类型,运行时照常执行,这样更省事。

正解声明文件的本质是“纯类型描述”,不能包含任何函数体、赋值逻辑或运行时产物。一旦混入实现,编译器可能把实现当类型信息处理,导致输出代码混乱,甚至造成运行时与类型不一致。正确做法是只保留签名和类型,把实现交给真正的 .ts 或 .js 文件。

为何常见:初学者刚接触时容易把 .d.ts 当成普通 TypeScript 文件,忽略了它“仅供编译期使用”的定位。

误区interface 能描述对象、函数、类的形状,那我干脆把库里的所有类型都用 interface 声明,不再用 declare var 或 declare class。

正解interface 描述的是“形状”,但它不参与声明“运行时存在某个名字”。变量、函数、类这类运行时实体必须用 declare var/function/class 声明,interface 只能用来补充它们的内部结构。二者是协作关系,不能互相替代。

为何常见:interface 在普通业务代码中出现频率最高,学习者容易形成“它什么都行”的错觉。

误区`/// <reference types="node" />` 看起来就像注释,我随手写一下,或干脆不写,应该都不影响编译结果。

正解三斜线指令虽然以 /// 开头,但它是编译器指令,会在预处理阶段被解析。错误引用会导致类型加载顺序出错或找不到声明,指令放错位置还会被静默忽略,造成“类型时有时无”的怪异现象。

为何常见:因为它长得实在太像普通注释,初学者会下意识忽略其语义作用。

误区我写了一个 `@types/my-custom-ai-lib`,里面放了我的库声明,别人只要安装这个包就能获得类型,名字不重要。

正解@types/* 包必须与被描述的库同名,例如 @types/jquery 对应 jquery,@types/node 对应 node。TS 在解析 import "jquery" 时会自动去 @types/jquery 查找声明。命名不匹配,编译器根本不会关联到你的声明。

为何常见:只看到 @types 前缀,没理解它与库名之间的强绑定关系。

误区我在 `.d.ts` 里写了 `import { x } from "foo"`,然后继续在文件里 `declare const api: ...`,觉得它仍然是全局的。

正解一旦文件包含 import 或 export,它就被视为模块,文件内的 declare 全部变成模块局部声明,不再自动全局可见。若想补充全局类型,必须把内容写进 declare global { } 块中。

为何常见:混淆了“脚本文件”和“模块文件”的边界,这是 .d.ts 中非常隐蔽又常见的认知陷阱。

面试题

Q1. .d.ts 声明文件的核心作用是什么?

单选题 · easy

  • A.编译时提供类型信息的纯描述文件← 正确答案
  • B.运行时增强程序性能的高级缓存
  • C.自动注入样式与行为的插件脚本
  • D.替代所有业务逻辑的自动生成器

答案:A。.d.ts 是编译期的“类型标签”,不参与运行时执行。它只描述类型形状,帮助编译器检查代码,A 正确。B、C、D 描述的都是运行时行为,与声明文件定位完全不符。

Q2. 在 .d.ts 文件中同时写了 import 又想补充全局类型,正确做法是?

单选题 · single

  • A.使用 declare global 包裹需要变全局的类型← 正确答案
  • B.使用 export {} 空导出就可以恢复全局性
  • C.将 import 改为三斜线 reference path 即可
  • D.删除所有 import 才能声明全局接口

答案:A。文件包含 import 后是模块,declare global 是唯一在模块内声明全局类型的正规手段。B 虽然能把文件变成模块,但不会让局部声明变全局;C、D 的说法都不准确。

Q3. 声明文件里最不适合编写的内容是什么?

单选题 · single

  • A.函数参数与返回值的类型签名
  • B.变量常量的类型描述和接口形状
  • C.函数的具体实现逻辑和业务代码← 正确答案
  • D.类构造函数参数和成员属性的类型

答案:C。声明文件是“纯描述”,严禁出现函数体、业务逻辑等实现内容。A、B、D 都是声明文件的合法职责,只有 C 违反了 ambient declaration 的根本原则。

Q4. declare module "*.png" 指令的作用是什么?

单选题 · single

  • A.为任意以 .png 结尾的模块声明统一类型形状← 正确答案
  • B.生成可执行的图片压缩优化算法代码
  • C.将图片资源预加载并进行浏览器缓存
  • D.替代 webpack 处理图片打包的完整配置

答案:A。通配符模块 *.png 让所有以 .png 结尾的导入路径都拥有声明中的类型,解决 TS 不认识图片模块的问题。B、C、D 都是运行时或构建期行为,与声明无关。

Q5. 三斜线指令 /// <reference types="node" /> 的作用是?

单选题 · single

  • A.引入 Node.js 官方的全局环境类型定义← 正确答案
  • B.在运行时加载 Node.js 的核心功能模块
  • C.强制将源码编译为 Node.js 可运行的格式
  • D.把声明接口自动实例化为 JavaScript 对象

答案:A。三斜线 types 指令是在编译期引入对应 @types/node 的全局声明,让 process、Buffer 等 Node 全局变量有类型。B、C、D 描述的运行时行为完全错误。

Q6. 以下哪些语法可用于声明文件里的顶层类型声明?

单选题 · multiple

  • A.declare var version: string; 声明变量类型← 正确答案
  • B.declare function show(): void; 描述函数← 正确答案
  • C.declare class Widget {} 描述类成员← 正确答案
  • D.console.log(version) 打印变量内容

答案:ABC。A、B、C 分别是变量、函数、类的标准声明写法。D 是运行时语句,声明文件不允许出现执行逻辑,因此错误。

Q7. 某个 .d.ts 文件同时含 import 语句与 declare global {} 块,关于这份文件的类型声明,正确的说法是?

单选题 · single

  • A.该文件被编译器纳入后,全局声明才会生效← 正确答案
  • B.全局声明必须等运行时 import 完成后才生效
  • C.import 会让整个文件在编译时被完全忽略
  • D.declare global 内的声明只能由全局脚本消费

答案:A。declare global 的生效条件是“文件被包含进编译”,它向全局类型环境追加声明。这是静态编译行为,不依赖运行时 import 顺序。B 混淆了编译期与运行时,C 完全错误,D 说模块无法消费也是错误的。

Q8. 要让你发布的 npm 库被 TypeScript 自动识别类型入口,应在 package.json 中配置哪个字段?

单选题 · single

  • A.main 字段指向运行时入口文件位置
  • B.types 字段指向 .d.ts 主声明文件← 正确答案
  • C.files 字段列出所有需要打包的文件
  • D.module 字段指向 ES 模块版本入口

答案:B。types(或旧名 typings)字段专门告诉 TypeScript 该包的主声明文件位置。main 和 module 是运行时入口,files 是发布白名单,它们都不承担类型入口职责。

Q9. 在 tsconfig.json 中,哪些配置会影响声明文件的加载?

单选题 · multiple

  • A.include 字段指定纳入编译的文件列表← 正确答案
  • B.types 字段限定自动加载的全局类型包← 正确答案
  • C.typeRoots 字段自定义类型包搜索目录← 正确答案
  • D.strict 字段开启更强的类型检查规则

答案:ABC。A 控制哪些 .d.ts 文件被纳入,B 和 C 控制 @types/* 包的选择与查找路径。D 虽然增强类型检查,但不决定加载哪些声明文件,因此不属于影响加载的配置。

Q10. 关于 .d.ts 中 declare module 和全局声明的组合语义,以下哪些说法是正确的?

单选题 · multiple

  • A.declare module "x" 允许在无运行时依赖时导入 "x"← 正确答案
  • B.declare global 只能出现在带有 import/export 的模块内← 正确答案
  • C.不带 import/export 的声明文件会自动变成模块文件
  • D.declare module 内的导出可被 import 语句直接引用← 正确答案

答案:ABD。A 是 ambient module 的核心能力;B 正确,全局增强必须位于模块中;D 正确,模块声明的导出体就是给 import 消费的。C 说反了——不带 import/export 是全局脚本,加上才变成模块,因此 C 错误。