从 LeetCode 实战入手,从零学习 TypeScript。记录了实现过程中的笔记和疑问。

1. 实现代码

ts
function twoSum(nums: number[], target: number): number[] {
    const hashtable = new Map<number, number>();
    const n = nums.length;
    for (let i = 0; i < n; ++i) {
        const num = nums[i];
        const index = hashtable.get(target - num);
        // Equality Narrowing
        if (index !== undefined) {
            return [i, index];
        }
        hashtable.set(num, i);
    }
    return [];
}

题目保证恰好存在一组解,且同一个元素不能使用两次;末尾的 return [] 仅作无解时的兜底处理。题目说明

2. 笔记

2.1 let、var、const 和变量类型

TS 中常用的变量声明方式是 let、var 和 const。变量名后可以使用 : T 标注类型,例如 let num: number = 0 或 let arr: number[] = []。有初始化值时,通常可以省略类型标注,由编译器推断;之后的赋值仍需符合显式声明或推断出的类型。

2.1.1 let, var, const
写法作用域能否重新赋值常用选择
let块作用域可以需要改变变量的值
const块作用域不可以默认优先使用
var在函数内声明时为函数作用域,不受普通 {} 限制可以旧代码常见,新代码通常避免
ts
let i = 0;
const table = new Map<number, number>();

const 是变量声明关键字,用于建立不能重新赋值的绑定,不能与 let、var 叠加使用。C++ 的 const 是类型限定符,两者的作用不同。

另外,const 限制的是变量重新赋值,仍允许修改对象内容:

ts
const table = new Map<number, number>();
table.set(8, 0);                   // 可以:修改现有对象
table = new Map<number, number>(); // 报错:重新赋值

这也是工程中经常使用 const 声明容器的原因。参考文档

2.1.2 声明和定义变量

TypeScript 提供静态类型检查,后续赋值必须符合变量的声明类型。例如,声明为 number | string 就允许数值和字符串;下面的 i 被推断为 number,因此不能赋值为字符串。类型标注会在生成 JavaScript 时被擦除,不会自动产生运行时的数据校验。

ts
let i = 0;         // 推断为 number
let j: number = 0; // 显式指定 number

i = "hello";       // 类型检查报错

上例省略类型标注时的类型推断,类似于 C++ 的 auto;let 本身是变量声明关键字,推断得到的类型仍会约束后续赋值。

像 twoSum 这样的独立函数,一般显式标注参数类型。回调参数可以由上下文推断,有默认值的参数也可以从默认值推断;没有这些依据且未标注类型时,启用 noImplicitAny 会报错。返回类型可以从返回表达式推断,也可以显式标注以明确接口约定。类型推断

2.2 new Map():采用构造器创建对象

TS 中,带初始化表达式的变量声明会把右侧表达式的结果绑定到变量名:

ts
const nums: number[] = [];                 // 数组字面量创建数组
const table = new Map<number, number>();   // 构造器创建 Map

数组有 [] 这种字面量语法;Map 没有专属字面量语法,构造器要求使用 new,直接调用 Map() 会报错。Map<number, number> 中的两个类型参数分别指定键和值的类型。Map 构造器

这里的 new 表示构造调用,对象内存由垃圾回收机制管理。JavaScript 的 delete 用于删除对象属性,并不是与 new 配对的内存释放操作,因此不能照搬 C++ 中配对管理对象内存的 new / delete 写法。内存管理、delete 运算符

2.3 类型收窄

Map.get(key) 返回键对应的值,键不存在时返回 undefined。本例的值类型为 number,因此 get() 的返回类型是 number | undefined。get 方法

Map.has(key) 检查键是否存在。通用的 Map 可以存储 undefined 值,因此键存在不一定意味着值不是 undefined;本例的 Map<number, number> 不包含这类值,但 TypeScript 也不会根据 has() 的检查,把随后一次 get() 调用的返回类型收窄为 number。has 方法

下面是本例中两个方法的类型签名片段,省略所属接口:

ts
has(key: number): boolean;
get(key: number): number | undefined;

具体到题目:

ts
const index = hashtable.get(target - num); // number | undefined

if (index !== undefined) {
    return [i, index]; // 此分支中,index 已确定为 number
}

TS 根据条件排除了变量 index 的 undefined 分支,这叫做类型收窄。这里的惯用思路是:先取得可能缺失的值,再检查它,随后使用这个已经检查过的值。 重新调用 get() 会得到一个仍可能是 undefined 的结果,之前对 index 的检查不会收窄这次调用。显式比较 undefined 也能正常保留下标 0。类型收窄