Day 16 - 读懂 TypeScript 声明文件(.d.ts)
学习目标:理解 ArkTS、TypeScript、JavaScript 三者的关系,掌握 .d.ts 声明文件的阅读方法,能够看懂 DevEco Studio 中的 API 提示,为 Day 17 的 Promise 课程做准备。
预计时间:1.5-2 小时
课前思考
在 DevEco Studio 中编写代码时,你一定见过这样的场景:
// 输入 console.log,悬停查看提示
console.log("Hello"); // 悬停显示:log(message?: any, ...optionalParams: any[]): void
// 输入 new Promise,Ctrl+Click 跳转到定义
new Promise<string>((resolve) => { // 跳转到 lib.es2015.promise.d.ts
resolve("done");
});
思考问题:
- 为什么 ArkTS 代码中会出现
any类型?我们学过 ArkTS 不是禁止any吗? - 为什么
new Promise能跳转到.d.ts文件,而不是.ets文件? - 为什么
.d.ts文件里的函数参数可以只写类型不写名字?这在 ArkTS 中不是不允许吗? - 为什么 Promise 的定义分散在多个
.d.ts文件中,它们是如何组合在一起的?
如果你对这些现象感到困惑,那么这节课就是为你准备的。
第一部分:ArkTS 的"幕后"——技术架构层次
1.1 ArkTS、TypeScript、JavaScript 的关系
问题:ArkTS 到底是什么?
要理解 .d.ts 文件,首先要理解 ArkTS 在技术栈中的位置。
三者的继承链
┌─────────────────────────────────────────────────────────────┐
│ JavaScript (JS) │
│ - 动态类型、运行时 │
│ - 所有代码最终在这里执行 │
├─────────────────────────────────────────────────────────────┤
│ TypeScript (TS) │
│ - 在 JS 上添加了静态类型系统 │
│ - 编译后变成 JS │
│ - .d.ts 是 TS 的类型声明标准 │
├─────────────────────────────────────────────────────────────┤
│ ArkTS │
│ - 在 TS 基础上增加了更严格的约束 │
│ - 专为鸿蒙应用开发设计 │
│ - 你的 .ets 文件属于这一层 │
└─────────────────────────────────────────────────────────────┘
C++ 类比(这是理解的关键):
| 层次 | 对应 C++ 世界 | 说明 |
|---|---|---|
| JavaScript | C 语言 | 底层执行环境,最接近机器 |
| TypeScript | C++ | 在 C 基础上添加类型系统,编译到 C |
| ArkTS | 某公司的 C++ 编码规范 | 在 C++ 基础上增加约束(如禁用某些特性) |
就像某些公司会规定"C++ 代码禁止使用裸指针"、"必须使用智能指针"一样,ArkTS 就是"TypeScript 的一个严格子集"。
你写的 .ets 最终跑在哪里?
你的代码:Index.ets (ArkTS)
↓
编译器:ArkCompiler
↓
中间产物:JavaScript 字节码
↓
运行时:鸿蒙 JS 引擎 (基于 QuickJS)
↓
执行结果
关键认知:
- 无论 ArkTS 多严格,最终都要编译成 JavaScript 才能执行
.d.ts文件是 TypeScript 生态的标准,ArkTS 复用了这套基础设施- 所以你看到的 API 声明都是
.d.ts格式,而不是.ets格式
1.2 .d.ts 声明文件是什么
问题:.d.ts 文件和 .ets 文件有什么区别?
类比 C++ 的头文件
在 C++ 中,你有 .h 头文件和 .cpp 源文件:
// math.h - 只有声明,没有实现
#ifndef MATH_H
#define MATH_H
int add(int a, int b); // 声明:有这个函数
int multiply(int a, int b); // 声明:有这个函数
#endif
// math.cpp - 实现
#include "math.h"
int add(int a, int b) {
return a + b; // 实际实现
}
int multiply(int a, int b) {
return a * b; // 实际实现
}
.d.ts 文件就相当于 C++ 的 .h 头文件:
// math.d.ts - 只有类型声明
export function add(a: number, b: number): number;
export function multiply(a: number, b: number): number;
// math.ets - 实际实现
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
.d.ts 的核心作用
| 作用 | 说明 |
|---|---|
| 类型描述 | 告诉编译器"某个东西存在,长什么样" |
| 无实现 | 只包含类型信息,不包含实际代码 |
| 编译时 | 只在编译阶段使用,不会打包到最终产物 |
| 描述已有 | 描述的是运行时已经存在的东西 |
.d.ts 的三种来源
在 DevEco Studio 项目中,你会遇到三种来源的 .d.ts 文件:
1. 内置类型(TypeScript 标准库)
路径示例(在你的 DevEco Studio 安装目录中):
DevEco Studio/plugins/arkts-language-server/node_modules/typescript/lib/
├── lib.es5.d.ts // ES5 基础类型
├── lib.es2015.d.ts // ES2015 新增特性
├── lib.es2015.promise.d.ts // Promise 定义
├── lib.dom.d.ts // DOM API(ArkTS 中不可用)
└── ...
2. 鸿蒙系统 API
路径示例(在项目依赖中):
oh_modules/@ohos/hypium/index.d.ts // 测试框架类型
oh_modules/@ohos/hamock/index.d.ts // Mock 框架类型
3. 第三方库类型
如果你引入 npm 包,通常会有对应的 .d.ts 文件:
oh_modules/some-library/index.d.ts
实现在哪里?
既然 .d.ts 只有声明,那实际实现在哪里?
| API 类型 | 声明位置 | 实际实现位置 |
|---|---|---|
| 基础类型(Array、String) | lib.es5.d.ts | 鸿蒙 JS 引擎(C++ 编写) |
| Promise | lib.es2015.promise.d.ts | 鸿蒙 JS 引擎(C++ 编写) |
| 系统 API | @ohos.xxx.d.ts | 系统 Native 层(C++) |
| 第三方库 | index.d.ts | 编译后的 .js 文件 |
1.3 为什么 API 用 .d.ts 而不是 .ets 定义
问题:既然 ArkTS 是独立语言,为什么不用 .ets 定义 API?
原因 1:生态复用
.d.ts 是 TypeScript 生态的通用标准。全世界的前端库都用这个格式提供类型声明。
ArkTS 复用了这套基础设施,意味着:
- 可以直接使用现有的类型定义工具
- 开发者学习一次,到处使用
- 与 JavaScript/TypeScript 生态保持兼容
原因 2:编译器架构
TypeScript 编译器
├── 解析 .ts/.d.ts 文件
├── 类型检查
└── 输出 .js
ArkTS 编译器
├── 解析 .ets/.d.ts 文件 ← 复用了 TS 的解析器
├── 类型检查(更严格)
└── 输出 字节码
ArkTS 编译器在底层复用了 TypeScript 的类型系统,自然使用相同的声明文件格式。
C++ 类比
就像 Linux 内核用 C 编写,但它的头文件可以被 C++ 项目使用:
// Linux 内核头文件(C 语言)
#include <linux/types.h> // 定义了 __u32、__u64 等
// C++ 驱动程序可以使用这些头文件
class MyDriver {
__u32 deviceId; // 使用 C 头文件中定义的类型
};
同样:
- TypeScript 定义了
.d.ts标准 - ArkTS 可以使用这些
.d.ts文件 - 尽管 ArkTS 比 TypeScript 更严格
1.4 DevEco Studio 中如何查看 .d.ts
实操:学会在 IDE 中探索类型定义
方法 1:Ctrl + Click(跳转到定义)
将光标放在任意 API 上,按住 Ctrl 键点击:
// 光标放在 "log" 上,Ctrl+Click
console.log("Hello");
// ↑ 跳转到 lib.es5.d.ts
// 光标放在 "Promise" 上,Ctrl+Click
new Promise(() => {});
// ↑ 跳转到 lib.es2015.promise.d.ts
方法 2:悬停查看类型提示
将鼠标悬停在代码上,IDE 会显示类型信息:
let arr: number[] = [1, 2, 3];
arr.push(4); // 悬停显示:push(...items: number[]): number
方法 3:Go to Type Definition(查看完整类型)
右键点击类型名 → Go to Type Definition:
let p: Promise<string>; // 右键 "Promise" → Go to Type Definition
// 跳转到 Promise 的完整接口定义
实操练习
请在你的 DevEco Studio 中尝试以下操作:
- 输入
console.log,Ctrl+Click 跳转到lib.es5.d.ts,看看console是怎么定义的 - 输入
let arr: number[] = []; arr.push(1);,悬停在push上查看签名 - 输入
new Promise<string>((resolve) => resolve("ok"));,Ctrl+Click 跳转到 Promise 定义
【第一部分小结】
| 概念 | 说明 | C++ 类比 |
|---|---|---|
| ArkTS | TypeScript 的严格子集 | 公司的 C++ 编码规范 |
| TypeScript | JavaScript + 类型系统 | C++(C + 类型系统) |
| JavaScript | 运行时执行环境 | C 语言 |
| .d.ts | 类型声明文件 | .h 头文件 |
| declare | 声明已存在的东西 | extern 声明 |
关键认知转变:
.d.ts是 TypeScript 标准,不是 ArkTS 标准- 你在 DevEco Studio 中看到的 API 提示都来自
.d.ts文件 - 这些文件描述的是"运行时已经存在的东西"
第二部分:.d.ts 与 ArkTS 的规则差异
这是本课最重要的部分。当你用 ArkTS 规则去解读 .d.ts 文件时,会遇到大量"看似违规"的写法。本节将逐一解释这些差异。
2.1 declare 关键字——"这东西已经存在了"
问题:.d.ts 文件里的 declare 是什么意思?
什么是 declare
declare 关键字表示:"这个东西已经存在了,我只是描述它的类型"。
// lib.es5.d.ts 中的 console 声明
declare var console: Console;
// 含义:运行时已经有一个叫 console 的变量,类型是 Console
// 注意:这里没有创建 console,只是声明它存在
C++ 类比:extern 声明
在 C++ 中,extern 用于声明一个在其他地方定义的变量:
// header.h
extern int globalCounter; // 声明:有个全局变量叫 globalCounter
// 实际定义在某个 .cpp 文件中
void useCounter(); // 函数声明
// source.cpp
int globalCounter = 0; // 实际定义在这里
void useCounter() {
globalCounter++; // 使用外部声明的变量
}
ArkTS/TypeScript 的 declare 完全等价:
// types.d.ts
declare var globalCounter: number; // 声明:运行时已有这个变量
declare function useCounter(): void; // 声明:运行时已有这个函数
// app.ets(实际运行时提供)
let globalCounter: number = 0; // 实际定义
function useCounter(): void {
globalCounter++;
}
declare 的三种常见形式
// 1. declare var - 声明变量(运行时已有)
declare var console: Console;
declare var window: Window;
// 2. declare function - 声明函数(运行时已有)
declare function setTimeout(handler: () => void, timeout: number): number;
// 3. declare class - 声明类(运行时已有)
declare class Promise<T> {
constructor(executor: (resolve: (value: T) => void, reject: (reason?: any) => void) => void);
}
关键区别:declare vs 普通声明
| 特性 | declare 声明 | 普通声明 |
|---|---|---|
| 是否创建值 | ❌ 不创建,只描述 | ✅ 创建实际值 |
| 编译后 | 完全消失 | 生成对应代码 |
| 用途 | 描述运行时已有 | 定义新的东西 |
| 使用场景 | .d.ts 文件 | .ets/.ts 文件 |
// .d.ts 文件中的 declare
declare var PI: number; // 只是声明,不创建 PI
// .ets 文件中的普通声明
const PI: number = 3.14159; // ✅ 创建实际的常量
// ❌ 在 .ets 中这样写是错误的
declare var PI: number; // 错误:.ets 中不能用 declare 创建变量
2.2 any 出现在 .d.ts 中
问题:ArkTS 不是禁止 any 吗?为什么 .d.ts 里到处都是 any?
现象观察
在 DevEco Studio 中悬停查看 console.log:
console.log("Hello");
// 悬停显示:log(message?: any, ...optionalParams: any[]): void
// ↑ any? ↑ any[]?
你会看到 any 类型!但我们在 Day 01 学过,ArkTS 禁止使用 any。
解释:两套规则
| 场景 | 规则 | 说明 |
|---|---|---|
| 写 ArkTS 代码(.ets) | 禁止 any | ArkTS 的严格规则 |
| 读 .d.ts 文件 | 允许 any | TypeScript 的标准规则 |
原因:.d.ts 文件遵循的是 TypeScript 规则,不是 ArkTS 规则。
TypeScript 允许 any,所以它的标准库声明文件里用了 any。ArkTS 在编译时会处理这些差异。
你在调用时不需要写 any
// .d.ts 中的声明(允许 any)
declare function processData(data: any): void;
// 你的 ArkTS 代码(禁止 any)
processData("Hello"); // ✅ 传 string,自动兼容
processData(123); // ✅ 传 number,自动兼容
processData({} as any); // ❌ ArkTS 中不能这样写!
关键理解:
.d.ts里的any表示"可以接受任何类型"- 你调用时传具体类型(string、number 等)即可
- ArkTS 编译器会自动处理这种兼容
规则对比表
| 规则 | ArkTS(.ets) | TypeScript(.d.ts) |
|---|---|---|
| any 类型 | ❌ 禁止 | ✅ 允许 |
| 用途 | 强制类型安全 | 兼容 JavaScript |
| 你的应对 | 传具体类型 | 理解其含义即可 |
2.3 回调函数参数可以少传
问题:为什么 Promise 的 executor 可以只写 resolve,不写 reject?
现象观察
// .d.ts 中的 Promise 构造函数声明
interface PromiseConstructor {
new <T>(executor: (resolve: (value: T) => void, reject: (reason?: any) => void) => void): Promise<T>;
}
// 你的代码
new Promise<string>((resolve) => { // ✅ 只写 resolve,不写 reject
resolve("ok");
});
在 .d.ts 中,resolve 和 reject 都是 executor 的参数。但你在使用时可以只写 resolve,不写 reject。
这在 ArkTS 中似乎违反了"函数参数必须完整"的规则!
原理:TypeScript 函数兼容性规则
TypeScript 有一个特殊的函数兼容性规则:
参数少的函数可以赋值给参数多的函数类型
// 定义:需要一个两个参数的函数
type TwoParamFunc = (a: number, b: number) => void;
// 实际:只用一个参数的函数
const oneParamFunc = (a: number) => {
console.log(a);
};
// ✅ TypeScript 允许这样赋值
const fn: TwoParamFunc = oneParamFunc;
// 调用时传两个参数
fn(1, 2); // 没问题,第二个参数被忽略
C++ 类比:std::for_each 的 lambda
在 C++ 中,也有类似的概念:
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// for_each 传递三个参数给 lambda:(element, index, container)
// 但你的 lambda 可以只用一个参数
std::for_each(vec.begin(), vec.end(), [](int element) {
std::cout << element << std::endl; // 只用了 element
// index 和 container 被忽略,这是合法的
});
return 0;
}
Promise executor 的实际例子
// .d.ts 中的声明(两个参数)
new <T>(executor: (resolve: (value: T) => void, reject: (reason?: any) => void) => void): Promise<T>;
// 你的调用方式(多种都合法)
// 方式1:只用 resolve
new Promise<string>((resolve) => {
resolve("ok");
});
// 方式2:resolve 和 reject 都用
new Promise<string>((resolve, reject) => {
if (Math.random() > 0.5) {
resolve("ok");
} else {
reject("error");
}
});
// 方式3:只用 reject(虽然不常见,但合法)
new Promise<string>((_, reject) => {
reject("error");
});
规则对比表
| 规则 | ArkTS 常规代码 | .d.ts 回调类型 |
|---|---|---|
| 参数数量 | 必须完全匹配 | 可以少传 |
| 原理 | 严格类型检查 | 函数兼容性 |
| C++ 类比 | 函数签名必须一致 | lambda 可忽略参数 |
2.4 interface 里的 new 和函数调用签名
问题:Promise 是 interface,为什么能用 new Promise()?
现象观察
// Ctrl+Click Promise,你会看到类似这样的定义
interface PromiseConstructor {
new <T>(executor: ...): Promise<T>; // 构造签名
}
declare var Promise: PromiseConstructor;
// 你的代码
new Promise(() => {}); // ✅ 能用 new!
Promise 被声明为 interface,但却可以用 new 创建实例。这在 C++ 中是不可思议的!
解释:构造签名(Constructor Signature)
在 TypeScript 中,interface 可以描述"可被 new 调用的东西":
interface MyConstructor {
new (name: string): MyInstance; // 构造签名:表示"可以被 new"
}
// 使用
let MyClass: MyConstructor = ...;
let instance = new MyClass("test"); // ✅ 合法
PromiseConstructor 的完整解释
// lib.es2015.promise.d.ts 中的简化版
interface PromiseConstructor {
// 构造签名:表示 Promise 可以被 new
new <T>(executor: (resolve: (value: T) => void, reject: (reason?: any) => void) => void): Promise<T>;
// 静态方法
all<T>(values: Iterable<Promise<T>>): Promise<T[]>;
race<T>(values: Iterable<Promise<T>>): Promise<T>;
resolve<T>(value: T): Promise<T>;
reject<T>(reason?: any): Promise<T>;
}
// 声明:运行时有一个叫 Promise 的变量,类型是 PromiseConstructor
declare var Promise: PromiseConstructor;
C++ 类比
C++ 中没有完全等价的机制,但最接近的是函数对象(重载 operator()):
// C++ 函数对象
class MyCallable {
public:
// 重载 operator() - 使对象可以被调用
int operator()(int x) {
return x * 2;
}
};
MyCallable callable;
int result = callable(5); // 调用 operator()
但 TypeScript 的 new 签名更强大:
new签名:描述"可以被 new 调用"- 调用签名:描述"可以被直接调用"
interface CallableConstructor {
new (): Instance; // 构造签名:new MyClass()
(): void; // 调用签名:myFunction()
}
调用签名(Call Signature)
除了 new 签名,interface 还可以描述普通函数:
interface MyFunction {
(x: number): string; // 调用签名:表示"可以被直接调用"
}
// 使用
let fn: MyFunction = (x) => x.toString();
let result: string = fn(42); // ✅ 直接调用
规则对比表
| 特性 | C++ | TypeScript(.d.ts) |
|---|---|---|
| 构造函数 | class 定义 | interface + new 签名 |
| 函数对象 | operator() | interface + 调用签名 |
| 分离声明 | .h + .cpp | .d.ts + 运行时实现 |
【第二部分小结】
| 差异点 | ArkTS 规则 | .d.ts 规则 | 你的应对 |
|---|---|---|---|
| declare | ❌ 不允许 | ✅ 广泛使用 | 理解其含义:"已存在" |
| any 类型 | ❌ 禁止 | ✅ 允许 | 调用时传具体类型 |
| 回调参数 | 必须完整 | 可以少传 | 按需使用,不传的不写 |
| interface + new | ❌ 不支持 | ✅ 构造签名 | 理解这是描述构造函数的方式 |
| 函数调用签名 | ❌ 不支持 | ✅ 支持 | 理解这是描述函数的方式 |
核心原则:
.d.ts遵循 TypeScript 规则,不是 ArkTS 规则- 读
.d.ts时,重点是理解"这个 API 怎么用",而不是"这个语法 ArkTS 支不支持" - 你在
.ets文件中写代码时,仍然要遵守 ArkTS 规则
第三部分:声明合并——一个类型,多个文件
3.1 TypeScript 标准库的文件组织
问题:为什么 Promise 的定义分散在多个文件中?
文件组织方式
TypeScript 标准库按 ECMAScript 版本分文件:
typescript/lib/
├── lib.es5.d.ts // ES5 基础(Array、String、Number 等)
├── lib.es2015.d.ts // ES2015 新增(Map、Set、WeakMap 等)
├── lib.es2015.promise.d.ts // ES2015 Promise
├── lib.es2015.iterable.d.ts // ES2015 迭代器
├── lib.es2018.d.ts // ES2018 新增
└── ...
为什么这样设计?
原因:向后兼容和渐进增强。
ES5 时代(2009):只有基础类型
↓
ES2015 时代:新增 Promise、Map、Set
↓
ES2018 时代:新增更多特性
↓
每个版本只定义该版本新增的内容
这样做的好处:
- 向后兼容:旧项目只用 ES5 的定义
- 渐进增强:新项目可以启用更多特性
- 避免重复:基础类型只定义一次
配置决定加载哪些文件
在你的项目中,module.json5 或 tsconfig.json 配置决定了加载哪些版本的定义:
// tsconfig.json 示例
{
"compilerOptions": {
"target": "ES2015", // 加载 ES2015 及之前的所有定义
"lib": ["ES2015", "DOM"]
}
}
3.2 声明合并规则
问题:同名 interface 为什么会自动合并?
什么是声明合并
在 TypeScript 中,如果定义了多个同名的 interface,它们会自动合并成一个:
// 文件 A
interface Box {
height: number;
width: number;
}
// 文件 B
interface Box {
scale: number;
}
// 实际效果(合并后)
interface Box {
height: number;
width: number;
scale: number;
}
// 使用
let box: Box = { height: 10, width: 20, scale: 1.5 }; // ✅ 需要所有属性
Promise 的实际例子
Promise 的定义分散在多个文件中,通过声明合并组合:
// lib.es5.d.ts - 基础定义(简化)
interface Promise<T> {
then<TResult>(onfulfilled: ...): Promise<TResult>;
catch(onrejected: ...): Promise<T>;
}
// lib.es2015.promise.d.ts - 扩展定义
interface Promise<T> {
finally(onfinally?: ...): Promise<T>; // ES2018 新增
}
// 实际效果(合并后)
interface Promise<T> {
then<TResult>(...): Promise<TResult>;
catch(...): Promise<T>;
finally(...): Promise<T>;
}
C++ 没有等价机制
重要认知:C++ 中没有声明合并机制!
// C++ 中这样写会报错:重复定义
// header1.h
struct Box {
int height;
int width;
};
// header2.h
struct Box {
int scale; // ❌ 错误:重复定义 Box
};
C++ 解决方式是继承或组合:
// C++ 的解决方案:继承
struct BoxBase {
int height;
int width;
};
struct Box : BoxBase {
int scale;
};
TypeScript 的声明合并是全新概念,C++ 开发者需要专门理解。
函数成员的合并规则
对于同名函数成员,TypeScript 会将其视为函数重载:
// 文件 A
interface Array<T> {
map<U>(callback: (value: T) => U): U[];
}
// 文件 B
interface Array<T> {
map<U>(callback: (value: T, index: number) => U): U[];
}
// 合并后(两个重载)
interface Array<T> {
map<U>(callback: (value: T) => U): U[];
map<U>(callback: (value: T, index: number) => U): U[];
}
3.3 如何找到一个类型的"完整面貌"
实操:在 DevEco Studio 中还原 Promise 的完整接口
步骤 1:跳转到类型定义
let p: Promise<string>;
// ↑ 右键 → Go to Type Definition
步骤 2:查看所有声明位置
DevEco Studio 会显示所有声明了 Promise 的文件:
Promise 的定义位置:
1. lib.es5.d.ts - 基础定义
2. lib.es2015.promise.d.ts - Promise 专用定义
3. lib.es2018.promise.d.ts - finally 方法
步骤 3:手动"合并"出完整接口
// ===== 从 lib.es5.d.ts =====
interface Promise<T> {
then<TResult>(onfulfilled: ...): Promise<TResult>;
catch(onrejected: ...): Promise<T>;
}
// ===== 从 lib.es2015.promise.d.ts =====
interface Promise<T> {
// (扩展,无新增实例方法)
}
// ===== 从 lib.es2018.promise.d.ts =====
interface Promise<T> {
finally(onfinally?: ...): Promise<T>;
}
// ===== 合并后的完整 Promise 接口 =====
interface Promise<T> {
then<TResult>(onfulfilled: ...): Promise<TResult>;
catch(onrejected: ...): Promise<T>;
finally(onfinally?: ...): Promise<T>;
}
PromiseConstructor 的合并
Promise 的构造函数和静态方法也在不同文件中定义:
// lib.es2015.promise.d.ts
interface PromiseConstructor {
new <T>(executor: ...): Promise<T>;
all<T>(values: Iterable<Promise<T>>): Promise<T[]>;
race<T>(values: Iterable<Promise<T>>): Promise<T>;
resolve<T>(value: T): Promise<T>;
reject<T>(reason?: any): Promise<T>;
}
declare var Promise: PromiseConstructor;
【第三部分小结】
| 概念 | 说明 | C++ 类比 |
|---|---|---|
| 声明合并 | 同名 interface 自动合并 | ❌ 无直接等价 |
| 文件组织 | 按 ES 版本分文件 | 类似版本化头文件 |
| 函数重载 | 同名函数合并为重载 | ✅ 有函数重载 |
| 查找方法 | Go to Type Definition | 查找所有引用 |
关键认知:
- 声明合并是 TypeScript 特有机制,C++ 没有
- 读 API 时要意识到它可能来自多个文件的合并
- 使用 IDE 的"Go to Type Definition"可以找到所有声明位置
第四部分:读懂复杂 API 签名
4.1 签名拆解三步法
问题:面对复杂的 API 签名,如何快速理解?
三步法概述
面对复杂的签名,按以下顺序阅读:
// 示例:Array.map 的签名
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
第一步:找到函数名和返回值(忽略泛型参数)
map<...>(...): U[];
// ↑ 函数名 ↑ 返回 U[]
第二步:看参数列表(? 表示可选,| 表示联合类型)
map(..., ...): U[];
// ↑ callbackfn: 回调函数
// ↑ thisArg?: 可选参数(? 表示可选)
第三步:回头看泛型参数(带默认值的通常可以不管)
map<U>(...): U[];
// ↑ U 是返回数组的元素类型,由回调返回值推断
C++ 类比:读 STL 模板函数
在 C++ 中读 STL 模板函数也是类似的方法:
// std::transform 的签名
template<class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first, InputIt last, OutputIt d_first, UnaryOperation unary_op);
// 阅读步骤:
// 1. 函数名:transform,返回 OutputIt
// 2. 参数:first, last(输入范围),d_first(输出位置),unary_op(操作函数)
// 3. 模板参数:InputIt, OutputIt, UnaryOperation(编译器自动推断)
4.2 常见 API 签名模式
模式 A:简单方法
// .d.ts 签名
method(arg: Type): ReturnType;
// 翻译成人话
"调用 method,传入 Type 类型的参数,返回 ReturnType"
// 实际调用
let result: ReturnType = obj.method(value);
实例:Array.push
// .d.ts 签名
push(...items: T[]): number;
// 翻译
"调用 push,传入任意数量的 T 类型参数,返回新数组长度"
// 实际调用
let arr: number[] = [1, 2];
let newLength: number = arr.push(3, 4, 5);
console.log(`${newLength}`); // 5
模式 B:回调型
// .d.ts 签名
method(callback: (value: T) => void): void;
// 翻译成人话
"调用 method,传入一个回调函数,该回调接收 T 类型的值,不返回"
// 实际调用
obj.method((value) => {
console.log(`${value}`);
});
实例:Array.forEach
// .d.ts 签名
forEach(callbackfn: (value: T, index?: number, array?: T[]) => void): void;
// 翻译
"遍历数组,对每个元素调用回调,回调接收值、索引、原数组(后两个可选)"
// 实际调用
let arr: number[] = [1, 2, 3];
// 只用 value
arr.forEach((value) => {
console.log(`${value}`);
});
// 用 value 和 index
arr.forEach((value, index) => {
console.log(`${index}: ${value}`);
});
模式 C:Promise 型
// .d.ts 签名
method(): Promise<ResultType>;
// 翻译成人话
"调用 method,返回一个 Promise,成功时携带 ResultType"
// 实际调用
method().then((result: ResultType) => {
console.log(`${result}`);
});
实例:fetch API(概念示例)
// .d.ts 签名
fetch(url: string): Promise<Response>;
// 翻译
"传入 URL,返回 Promise,成功时携带 Response 对象"
// 实际调用
fetch("https://api.example.com/data").then((response: Response) => {
console.log(`Status: ${response.status}`);
});
模式 D:重载型
// .d.ts 签名(多个签名)
method(arg: string): string;
method(arg: number): number;
method(arg: string | number): string | number;
// 翻译成人话
"method 支持多种调用方式,根据参数类型返回对应类型"
// 实际调用
let s: string = method("hello"); // 返回 string
let n: number = method(42); // 返回 number
实例:Array.slice
// .d.ts 签名
slice(): T[];
slice(start: number): T[];
slice(start: number, end: number): T[];
// 翻译
"slice 有三种用法:
1. 无参数:复制整个数组
2. 一个参数:从 start 到末尾
3. 两个参数:从 start 到 end"
// 实际调用
let arr: number[] = [1, 2, 3, 4, 5];
let copy1: number[] = arr.slice(); // [1,2,3,4,5]
let copy2: number[] = arr.slice(2); // [3,4,5]
let copy3: number[] = arr.slice(1, 3); // [2,3]
4.3 泛型参数在 API 签名中的作用
带默认值的泛型参数
// .d.ts 签名
function createArray<T = string>(length: number): T[];
// 翻译
"创建数组,元素类型默认为 string,也可以指定其他类型"
// 实际调用
let arr1 = createArray(5); // T 默认为 string,string[]
let arr2 = createArray<number>(5); // 显式指定 number,number[]
规律:带默认值的泛型参数通常可以不管,编译器会自动处理。
带约束的泛型参数
// .d.ts 签名
function process<T extends { name: string }>(item: T): void;
// 翻译
"处理一个对象,该对象必须有 name 属性,类型由传入值决定"
// 实际调用
process({ name: "Alice", age: 30 }); // ✅ 有 name 属性
// process({ age: 30 }); // ❌ 编译错误,缺少 name
多泛型参数
// .d.ts 签名
function map<T, U>(array: T[], fn: (item: T) => U): U[];
// 翻译
"将 T 数组映射为 U 数组,T 是输入类型,U 是输出类型"
// 实际调用
let numbers: number[] = [1, 2, 3];
let strings: string[] = map(numbers, (n) => n.toString());
// T = number, U = string,由编译器自动推断
4.4 从签名到调用——翻译练习
练习 1:Array.map
// .d.ts 签名
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
// 请写出调用代码,将 number[] 转换为 string[]
点击查看答案
let numbers: number[] = [1, 2, 3, 4, 5];
// 调用 map,将每个 number 转为 string
let strings: string[] = numbers.map((value: number) => {
return value.toString();
});
console.log(`${strings}`); // ["1", "2", "3", "4", "5"]
// 简化版(省略类型,编译器自动推断)
let strings2: string[] = numbers.map((v) => v.toString());
练习 2:Promise.all
// .d.ts 签名
all<T>(values: Iterable<Promise<T>>): Promise<T[]>;
// 请写出调用代码,等待多个 Promise 完成
点击查看答案
// 创建多个 Promise
let p1: Promise<number> = Promise.resolve(1);
let p2: Promise<number> = Promise.resolve(2);
let p3: Promise<number> = Promise.resolve(3);
// 等待所有 Promise 完成
Promise.all([p1, p2, p3]).then((results: number[]) => {
console.log(`${results}`); // [1, 2, 3]
});
练习 3:setTimeout
// .d.ts 签名
setTimeout(handler: () => void, timeout?: number): number;
// 请写出调用代码,延迟 1 秒后执行
点击查看答案
// 延迟 1 秒执行
let timerId: number = setTimeout(() => {
console.log("1秒后执行");
}, 1000);
// 可选:取消定时器
// clearTimeout(timerId);
【第四部分小结】
| 步骤 | 内容 | 技巧 |
|---|---|---|
| 第一步 | 找函数名和返回值 | 忽略泛型,看整体 |
| 第二步 | 看参数列表 | ? 表示可选, |
| 第三步 | 回头看泛型 | 带默认值的通常可以不管 |
常见模式速查:
| 模式 | 签名特征 | 调用方式 |
|---|---|---|
| 简单方法 | method(arg: T): R |
obj.method(value) |
| 回调型 | method(cb: (v: T) => void) |
obj.method((v) => {...}) |
| Promise 型 | method(): Promise<R> |
method().then((r) => {...}) |
| 重载型 | 多个同名签名 | 根据参数类型匹配 |
第五部分:ArkTS 类型推断与显式声明规则
5.1 为什么需要了解类型推断
在学习 Promise 之前,你必须先理解一个关键问题:
ArkTS 代码中,什么时候可以省略类型,什么时候必须显式声明?
这个问题直接影响你能否正确理解 Promise 的构造函数签名和实际调用方式。
5.2 类型推断的基本原理
类型推断是指:编译器根据上下文自动推导出变量或表达式的类型,你不需要手动写出来。
// 显式声明(你写出类型)
let name: string = "Alice";
// 类型推断(编译器自动推导)
let name = "Alice"; // 编译器推断 name 为 string 类型
C++ 类比:
C++11 引入了 auto 关键字,也是类型推断:
// C++ 显式声明
std::string name = "Alice";
// C++ 类型推断(auto)
auto name = "Alice"; // 编译器推断为 const char*
但 ArkTS 和 C++ 的 auto 有本质区别:
- C++ 的
auto是根据初始化表达式推导,编译后类型是确定的 - ArkTS 的类型推断也是静态推导,但规则更严格,且某些场景下推断可能不精确
5.3 ArkTS 中的类型推断规则
以下是你在学习 Promise 时会遇到的具体场景:
场景 A:变量初始化(推荐显式声明)
// 可以推断(简单场景)
let count = 42; // 推断为 number
let message = "hello"; // 推断为 string
// 推荐显式声明(复杂场景,避免推断错误)
let data: Promise<string> = fetchData(); // 明确知道返回值类型
let results: string[] = []; // 空数组推断为 never[],必须显式声明
规则:简单字面量可以省略类型,复杂类型建议显式声明。
场景 B:函数返回值(必须显式声明)
// ✅ 正确:显式声明返回值类型
function add(a: number, b: number): number {
return a + b;
}
// ❌ 不推荐:依赖推断
function add(a: number, b: number) {
return a + b; // 编译器能推断为 number,但不清晰
}
规则:函数返回值类型必须显式声明,这是 ArkTS 最佳实践。
场景 C:Promise 构造函数(重点)
// .d.ts 中的声明(两个参数)
new <T>(executor: (resolve: (value: T) => void, reject: (reason?: any) => void) => void): Promise<T>;
// 方式1:只写 resolve(TypeScript 允许,因为函数兼容性)
new Promise<string>((resolve) => {
resolve("ok");
});
// 方式2:显式声明 resolve 类型(推荐,更清晰)
new Promise<string>((resolve: (value: string) => void) => {
resolve("ok");
});
// 方式3:resolve 和 reject 都显式声明(最完整)
new Promise<string>(
(resolve: (value: string) => void, reject: (reason: Error) => void) => {
if (Math.random() > 0.5) {
resolve("ok");
} else {
reject(new Error("fail"));
}
}
);
规则:
new Promise<T>()的泛型参数 建议显式声明。ArkTS 在部分场景下支持泛型推断(例如能从参数类型确定具体类型时),但推断能力有限,推断失败时必须手动补上<T>resolve和reject参数的类型可以省略(TypeScript 函数兼容性允许)- 在教学中,我们推荐显式声明,帮助理解类型对应关系
场景 D:回调函数参数(视情况)
// .d.ts 签名
forEach(callbackfn: (value: T, index?: number, array?: T[]) => void): void;
// 方式1:省略参数类型(编译器从上下文推断)
arr.forEach((v) => console.log(v));
// 方式2:显式声明参数类型(推荐)
arr.forEach((v: number) => console.log(v));
规则:回调参数可以从上下文推断类型,但显式声明更清晰。
场景 E:链式调用的返回值
// 每个 then 返回新的 Promise,类型从回调返回值推断
let p: Promise<number> = Promise.resolve(10);
let p2: Promise<string> = p.then((value: number) => {
return "数字:" + value.toString(); // 返回值推断为 string,p2 为 Promise<string>
});
规则:链式调用中,返回类型由回调的返回值自动推断,通常不需要显式声明。
5.4 显式类型声明 vs 类型推断——决策表
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 变量初始化(简单字面量) | 可以推断 | let x = 42 足够清晰 |
| 变量初始化(空数组/对象) | 显式声明 | let arr = [] 推断为 never[] |
| 函数参数 | 显式声明 | ArkTS 要求,也是可读性需要 |
| 函数返回值 | 显式声明 | ArkTS 最佳实践 |
Promise 泛型 <T> |
建议显式声明 | ArkTS 泛型推断能力有限,推断失败时需手动补上 <T> |
| Promise executor 参数 | 可以推断 | TypeScript 函数兼容性允许 |
| 回调函数参数 | 推荐显式 | 特别是复杂回调 |
| 链式调用 then 返回值 | 可以推断 | 从回调返回值自动推导 |
5.5 一个完整的 Promise 显式类型示例
// 最完整的显式声明写法(教学推荐)
function fetchUser(userId: number): Promise<string> {
return new Promise<string>(
(resolve: (value: string) => void, reject: (reason: Error) => void): void => {
setTimeout((): void => {
if (userId <= 0) {
reject(new Error("无效的用户ID"));
} else {
resolve("用户" + userId.toString() + "的数据");
}
}, 1000);
}
);
}
// 调用
fetchUser(100)
.then((data: string): void => {
console.log("成功:" + data);
})
.catch((error: Error): void => {
console.log("失败:" + error.message);
});
说明:
- 所有类型都显式声明,一眼就能看出每个部分的类型
- 对于初学者,这是理解 Promise 类型系统的最佳方式
- 熟练后,部分类型可以省略(但 Promise 泛型必须保留)
第六部分:实战——用学到的方法读懂 Promise API
6.1 还原 Promise 的完整类型定义
目标:从多个 .d.ts 文件中收集 Promise 的所有声明,合并为完整的"头文件"。
Promise 实例方法(来自 lib.es5.d.ts)
// ===== Promise 实例接口 =====
interface Promise<T> {
/**
* 链式调用,处理成功结果
*/
then<TResult>(
onfulfilled: ((value: T) => TResult | PromiseLike<TResult>) | null,
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<TResult>;
/**
* 捕获错误
*/
catch<TResult>(
onrejected: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<TResult>;
}
Promise 构造函数和静态方法(来自 lib.es2015.promise.d.ts)
// ===== Promise 构造函数接口 =====
interface PromiseConstructor {
// 构造签名
new <T>(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void
) => void
): Promise<T>;
// 静态方法:创建已解决的 Promise
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
resolve(): Promise<void>;
// 静态方法:创建已拒绝的 Promise
reject<T>(reason?: any): Promise<T>;
// 静态方法:等待所有 Promise
all<T>(values: Iterable<Promise<T>>): Promise<T[]>;
// 静态方法:竞速,返回最快完成的
race<T>(values: Iterable<Promise<T>>): Promise<T>;
}
// 声明 Promise 变量
declare var Promise: PromiseConstructor;
Promise.finally(来自 lib.es2018.promise.d.ts)
// ===== Promise 实例扩展(ES2018)=====
interface Promise<T> {
/**
* 无论成功失败都会执行
*/
finally(onfinally?: (() => void) | null): Promise<T>;
}
合并后的完整 Promise 定义
// ===== 完整 Promise 类型定义(合并后)=====
// 实例接口
interface Promise<T> {
then<TResult>(
onfulfilled: ((value: T) => TResult | PromiseLike<TResult>) | null,
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<TResult>;
catch<TResult>(
onrejected: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<TResult>;
finally(onfinally?: (() => void) | null): Promise<T>;
}
// 构造函数接口
interface PromiseConstructor {
new <T>(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void
) => void
): Promise<T>;
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
resolve(): Promise<void>;
reject<T>(reason?: any): Promise<T>;
all<T>(values: Iterable<Promise<T>>): Promise<T[]>;
race<T>(values: Iterable<Promise<T>>): Promise<T>;
}
declare var Promise: PromiseConstructor;
6.2 逐个翻译 Promise 的 API
new Promise(executor)
// 签名
new <T>(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void
) => void
): Promise<T>;
// 翻译成人话
"创建一个 Promise,传入 executor 函数。
executor 接收两个参数:
- resolve:调用表示成功,传入结果值
- reject:调用表示失败,传入错误原因(可选)"
// 实际调用
let p: Promise<string> = new Promise<string>((resolve, reject) => {
if (Math.random() > 0.5) {
resolve("成功"); // 调用 resolve 表示成功
} else {
reject("失败"); // 调用 reject 表示失败
}
});
.then(onfulfilled, onrejected)
// 签名
then<TResult>(
onfulfilled: ((value: T) => TResult | PromiseLike<TResult>) | null,
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<TResult>;
// 翻译成人话
"注册成功和失败的回调,返回新的 Promise。
onfulfilled:成功时调用,接收结果值
onrejected:失败时调用(可选),接收错误原因"
// 实际调用
p.then(
(value: string) => {
console.log(`成功: ${value}`);
return value.length;
},
(reason: any) => {
console.log(`失败: ${reason}`);
return 0;
}
);
.catch(onrejected)
// 签名
catch<TResult>(
onrejected: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<TResult>;
// 翻译成人话
"注册错误处理回调,相当于 .then(null, onrejected)"
// 实际调用
p.catch((reason: any) => {
console.log(`捕获错误: ${reason}`);
});
Promise.all(values)
// 签名
all<T>(values: Iterable<Promise<T>>): Promise<T[]>;
// 翻译成人话
"传入一个 Promise 数组,返回新的 Promise。
当所有 Promise 都成功时,新 Promise 成功,结果是所有结果的数组。
如果有任何一个失败,新 Promise 立即失败。"
// 实际调用
let p1: Promise<number> = Promise.resolve(1);
let p2: Promise<number> = Promise.resolve(2);
let p3: Promise<number> = Promise.resolve(3);
Promise.all([p1, p2, p3]).then((results: number[]) => {
console.log(`所有结果: ${results}`); // [1, 2, 3]
});
Promise.race(values)
// 签名
race<T>(values: Iterable<Promise<T>>): Promise<T>;
// 翻译成人话
"传入一个 Promise 数组,返回新的 Promise。
哪个 Promise 先完成(无论成功失败),新 Promise 就采用它的结果。"
// 实际调用
let fast: Promise<string> = new Promise((resolve) => {
setTimeout(() => resolve("快"), 100);
});
let slow: Promise<string> = new Promise((resolve) => {
setTimeout(() => resolve("慢"), 1000);
});
Promise.race([fast, slow]).then((result: string) => {
console.log(`胜出: ${result}`); // "快"
});
6.3 常见困惑点 Q&A
Q1:Promise 是 interface,为什么能用 new?
A:Promise 变量是 PromiseConstructor 类型,该 interface 有 new 签名。
// Promise 是 PromiseConstructor 类型
declare var Promise: PromiseConstructor;
// PromiseConstructor 有构造签名
interface PromiseConstructor {
new <T>(...): Promise<T>; // 这表示可以被 new
}
Q2:executor 的参数为什么可以只写一个?
A:TypeScript 函数兼容性规则允许参数少的函数赋值给参数多的函数类型。
// .d.ts 声明需要两个参数
new <T>(executor: (resolve: ..., reject: ...) => void): Promise<T>;
// 实际使用时可以只写一个
new Promise<string>((resolve) => { ... }); // ✅ 合法
Q3:.d.ts 里的 any 在 ArkTS 中怎么用?
A:调用时传具体类型即可,不需要写 any。
// .d.ts 中的声明
reject(reason?: any): Promise<T>;
// ArkTS 调用
reject("错误信息"); // ✅ 传 string
reject(404); // ✅ 传 number
Q4:PromiseLike 是什么?
A:PromiseLike 是"类 Promise 对象"的接口,任何有 then 方法的对象都符合。
// PromiseLike 定义(简化)
interface PromiseLike<T> {
then<TResult>(
onfulfilled: (value: T) => TResult | PromiseLike<TResult>
): PromiseLike<TResult>;
}
// 用途:允许 resolve 接受另一个 Promise
Promise.resolve(Promise.resolve(42)); // ✅ 合法
【第五部分小结】
| API | 核心作用 | 记忆口诀 |
|---|---|---|
| new Promise | 创建 Promise | "new 一个,传 executor,调 resolve/reject" |
| .then | 链式处理 | "then 接成功,可选接失败" |
| .catch | 错误处理 | "catch 专门抓错误" |
| Promise.all | 等待全部 | "all 等全部,race 争第一" |
| Promise.race | 竞争返回 | "race 谁快用谁" |
第七部分:小结与速查
7.1 .d.ts vs ArkTS 规则差异速查表
| 差异点 | .d.ts 规则 | ArkTS 规则 | 你的应对 |
|---|---|---|---|
| declare | ✅ 广泛使用 | ❌ 不允许使用 | 理解含义:描述已存在的东西 |
| any 类型 | ✅ 允许 | ❌ 禁止 | 调用时传具体类型 |
| 回调参数 | 可以少传 | 必须完整 | 按需使用,不传的不写 |
| interface + new | ✅ 构造签名 | ❌ 不支持 | 理解这是描述构造函数的方式 |
| 函数调用签名 | ✅ 支持 | ❌ 不支持 | 理解这是描述函数的方式 |
| 声明合并 | ✅ 自动合并 | ❌ 不支持 | 同名 interface 会合并 |
| 可选参数 ? | ✅ 支持 | ✅ 支持 | 标记为 ? 的参数可以省略 |
| 联合类型 | | ✅ 支持 | ✅ 支持 | 表示"或"的关系 |
7.2 类型推断与显式声明速查表
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 变量初始化(简单字面量) | 可以推断 | let x = 42 足够清晰 |
| 变量初始化(空数组/对象) | 显式声明 | let arr = [] 推断为 never[] |
| 函数参数 | 显式声明 | ArkTS 要求,也是可读性需要 |
| 函数返回值 | 显式声明 | ArkTS 最佳实践 |
Promise 泛型 <T> |
建议显式声明 | ArkTS 泛型推断能力有限,推断失败时需手动补上 <T> |
| Promise executor 参数 | 可以推断 | TypeScript 函数兼容性允许 |
| 回调函数参数 | 推荐显式 | 特别是复杂回调 |
| 链式调用 then 返回值 | 可以推断 | 从回调返回值自动推导 |
7.3 API 签名阅读速查卡
三步法:
- 找函数名和返回值(忽略泛型)
- 看参数列表(? 表示可选,| 表示联合)
- 回头看泛型(带默认值的通常不管)
常见模式:
| 模式 | 签名示例 | 调用示例 |
|---|---|---|
| 简单方法 | method(arg: T): R |
obj.method(value) |
| 回调型 | method(cb: (v: T) => void) |
obj.method((v) => {...}) |
| Promise 型 | method(): Promise<R> |
method().then((r) => {...}) |
| 重载型 | 多个同名签名 | 根据参数匹配 |
7.4 练习题
练习 1:理解 declare
下面的代码片段来自 .d.ts 文件,请解释 declare var console: Console; 的含义。
declare var console: Console;
interface Console {
log(message?: any, ...optionalParams: any[]): void;
}
点击查看答案
答案:
declare var console: Console;表示:运行时已经存在一个名为console的变量,它的类型是Console- 这不是在创建
console,而是在描述已经存在的console - 类比 C++ 的
extern声明:extern Console console;
练习 2:读懂回调签名
请解释以下签名,并写出调用代码:
function filter<T>(array: T[], predicate: (value: T, index?: number) => boolean): T[];
点击查看答案
答案:
- 函数名:
filter - 泛型参数:
T- 数组元素类型 - 参数1:
array: T[]- 要过滤的数组 - 参数2:
predicate- 回调函数,接收值和可选的索引,返回 boolean - 返回值:
T[]- 过滤后的数组
调用代码:
let numbers: number[] = [1, 2, 3, 4, 5];
// 只用 value
let evens: number[] = filter(numbers, (v) => v % 2 === 0);
// 用 value 和 index
let odds: number[] = filter(numbers, (v, i) => i % 2 === 0);
练习 3:理解声明合并
以下两个 interface 定义在不同文件中,请问合并后 Person 有哪些属性?
// file1.d.ts
interface Person {
name: string;
age: number;
}
// file2.d.ts
interface Person {
email: string;
}
点击查看答案
答案:
合并后的 Person 接口:
interface Person {
name: string;
age: number;
email: string;
}
同名 interface 会自动合并,所有属性合在一起。
练习 4:翻译 Promise 签名
请翻译以下 Promise 静态方法的签名,并写出调用代码:
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
点击查看答案
答案:
- 函数名:
resolve - 泛型参数:
T- Promise 成功时的值类型 - 参数:
value- 可以是 T 类型,或 PromiseLike类型 - 返回值:
Promise<T>- 一个已解决的 Promise
调用代码:
// 传入普通值
let p1: Promise<number> = Promise.resolve(42);
// 传入 Promise(会被展平)
let p2: Promise<number> = Promise.resolve(Promise.resolve(42));
p1.then((v: number) => {
console.log(`${v}`); // 42
});
练习 5:理解构造签名
以下代码定义了一个可以被 new 的 interface,请解释其含义:
interface MyConstructor {
new (name: string): MyInstance;
}
declare var MyClass: MyConstructor;
点击查看答案
答案:
MyConstructorinterface 有一个new签名,表示它可以被new调用new (name: string): MyInstance表示:调用时传入 string 参数,返回 MyInstance 实例declare var MyClass: MyConstructor;声明运行时有一个MyClass变量,类型是 MyConstructor- 因此可以写:
let obj: MyInstance = new MyClass("test");
练习 6:识别 API 模式
以下签名属于哪种模式?请写出调用代码。
fetch(url: string): Promise<Response>;
点击查看答案
答案:
- 模式:Promise 型
- 函数名:
fetch - 参数:
url: string- 要请求的 URL - 返回值:
Promise<Response>- 返回一个 Promise,成功时携带 Response
调用代码:
fetch("https://api.example.com/data").then((response: Response) => {
console.log(`Status: ${response.status}`);
});
练习 7:理解函数兼容性
以下代码为什么能编译通过?
// .d.ts 中的声明
new <T>(executor: (resolve: (value: T) => void, reject: (reason?: any) => void) => void): Promise<T>;
// 你的代码
new Promise<string>((resolve) => {
resolve("ok");
});
点击查看答案
答案:
.d.ts中声明的executor需要两个参数:resolve和reject- 但 TypeScript 的函数兼容性规则允许:参数少的函数可以赋值给参数多的函数类型
- 因此只传
resolve是合法的 - 这类似于 C++ 中 lambda 可以忽略不用的参数
练习 8:综合应用
请根据以下 .d.ts 签名,写出完整的调用代码:
interface Array<T> {
map<U>(callbackfn: (value: T, index: number, array: T[]) => U): U[];
filter(predicate: (value: T, index?: number) => boolean): T[];
}
要求:
- 创建一个 number 数组
- 使用 filter 过滤出大于 5 的数
- 使用 map 将结果转换为 string 数组
- 打印最终结果
点击查看答案
答案:
// 1. 创建 number 数组
let numbers: number[] = [1, 6, 2, 8, 3, 9, 4];
// 2. 过滤出大于 5 的数
let filtered: number[] = numbers.filter((v) => v > 5);
// 3. 转换为 string 数组
let strings: string[] = filtered.map((v) => v.toString());
// 4. 打印结果
console.log(`${strings}`); // ["6", "8", "9"]
// 或者链式调用
let result: string[] = numbers
.filter((v) => v > 5)
.map((v) => v.toString());
console.log(`${result}`); // ["6", "8", "9"]
C++ vs TypeScript 声明体系对照表
| 概念 | C++ | TypeScript(.d.ts) | 说明 |
|---|---|---|---|
| 声明文件 | .h 头文件 |
.d.ts 声明文件 |
只有声明,没有实现 |
| 实现文件 | .cpp 源文件 |
.js / 引擎内置 |
实际代码 |
| 外部声明 | extern |
declare |
声明已存在的东西 |
| 变量声明 | extern int x; |
declare var x: number; |
声明变量存在 |
| 函数声明 | int foo(); |
declare function foo(): number; |
声明函数存在 |
| 类声明 | class MyClass; |
declare class MyClass {...} |
声明类存在 |
| 构造签名 | 类定义本身 | new (): T |
TS 用 interface 描述 |
| 函数对象 | operator() |
调用签名 (): T |
使对象可调用 |
| 函数重载 | ✅ 支持 | ✅ 支持 | 同名不同参数 |
| 声明合并 | ❌ 不支持 | ✅ 支持 | TS 特有机制 |
| 泛型/模板 | template<T> |
<T> |
类型参数化 |
| 命名空间 | namespace |
namespace / module |
概念相同 |
| 默认参数 | int foo(int x = 0) |
foo(x?: number) |
可选参数 |
| 头文件包含 | #include |
import type / 自动加载 |
引入声明 |

浙公网安备 33010602011771号