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");
});

思考问题:

  1. 为什么 ArkTS 代码中会出现 any 类型?我们学过 ArkTS 不是禁止 any 吗?
  2. 为什么 new Promise 能跳转到 .d.ts 文件,而不是 .ets 文件?
  3. 为什么 .d.ts 文件里的函数参数可以只写类型不写名字?这在 ArkTS 中不是不允许吗?
  4. 为什么 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 中尝试以下操作:

  1. 输入 console.log,Ctrl+Click 跳转到 lib.es5.d.ts,看看 console 是怎么定义的
  2. 输入 let arr: number[] = []; arr.push(1);,悬停在 push 上查看签名
  3. 输入 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 中,resolvereject 都是 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 时代:新增更多特性
    ↓
每个版本只定义该版本新增的内容

这样做的好处:

  1. 向后兼容:旧项目只用 ES5 的定义
  2. 渐进增强:新项目可以启用更多特性
  3. 避免重复:基础类型只定义一次

配置决定加载哪些文件

在你的项目中,module.json5tsconfig.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"));
        }
    }
);

规则

  1. new Promise<T>() 的泛型参数 建议显式声明。ArkTS 在部分场景下支持泛型推断(例如能从参数类型确定具体类型时),但推断能力有限,推断失败时必须手动补上 <T>
  2. resolvereject 参数的类型可以省略(TypeScript 函数兼容性允许)
  3. 在教学中,我们推荐显式声明,帮助理解类型对应关系

场景 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?

APromise 变量是 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 是什么?

APromiseLike 是"类 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 签名阅读速查卡

三步法

  1. 找函数名和返回值(忽略泛型)
  2. 看参数列表(? 表示可选,| 表示联合)
  3. 回头看泛型(带默认值的通常不管)

常见模式

模式 签名示例 调用示例
简单方法 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;
点击查看答案

答案

  • MyConstructor interface 有一个 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 需要两个参数:resolvereject
  • 但 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[];
}

要求:

  1. 创建一个 number 数组
  2. 使用 filter 过滤出大于 5 的数
  3. 使用 map 将结果转换为 string 数组
  4. 打印最终结果
点击查看答案

答案

// 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 / 自动加载 引入声明
posted @ 2026-04-21 13:50  thammer  阅读(73)  评论(0)    收藏  举报