一、如何在 window 对象上显式设置属性

对于使用过 JavaScript 的开发者来说,对于 window.MyNamespace = window.MyNamespace || {}; 这行代码并不会陌生。为了避免开发过程中出现冲突,我们一般会为某些功能设置独立的命名空间。

然而,在 TS 中对于 window.MyNamespace = window.MyNamespace || {}; 这行代码,TS 编译器会提示以下异常信息:

Property 'MyNamespace' does not exist on type 'Window & typeof globalThis'.(2339)

以上异常信息是说在 Window & typeof globalThis 交叉类型上不存在 MyNamespace 属性。那么如何解决这个问题呢?最简单的方式就是使用类型断言:

(window as any).MyNamespace = {};

虽然使用 any 大法可以解决上述问题,但更好的方式是扩展 lib.dom.d.ts 文件中的 Window 接口来解决上述问题,具体方式如下:

declare interface Window {  MyNamespace: any;
}window.MyNamespace = window.MyNamespace || {};复制代码

二、如何为对象动态分配属性

在 JavaScript 中,我们可以很容易地为对象动态分配属性,比如:

let developer = {};
developer.name = "semlinker";复制代码

以上代码在 JavaScript 中可以正常运行,但在 TypeScript 中,编译器会提示以下异常信息:

Property 'name' does not exist on type '{}'.(2339)复制代码

{} 类型表示一个没有包含成员的对象,所以该类型没有包含 name 属性。为了解决这个问题,我们可以声明一个 LooseObject 类型:

interface LooseObject {
  [key: string]: any}复制代码

该类型使用 索引签名 的形式描述 LooseObject 类型可以接受 key 类型是字符串,值的类型是 any 类型的字段。有了 LooseObject 类型之后,我们就可以通过以下方式来解决上述问题:

interface LooseObject {
  [key: string]: any}let developer: LooseObject = {};
developer.name = "semlinker";复制代码

对于 LooseObject 类型来说,它的约束是很宽松的。在一些应用场景中,我们除了希望能支持动态的属性之外,也希望能够声明一些必选和可选的属性。

比如对于一个表示开发者的 Developer 接口来说,我们希望它的 name 属性是必填,而 age 属性是可选的,此外还支持动态地设置字符串类型的属性。针对这个需求我们可以这样做:

interface Developer {  name: string;
  age?: number;
  [key: string]: any}let developer: Developer = { name: "semlinker" };
developer.age = 30;
developer.city = "XiaMen";复制代码

其实除了使用 索引签名 之外,我们也可以使用 TypeScript 内置的工具类型 Record 来定义 Developer 接口:

// type Record = { [P in K]: T; }interface Developer extends Record<string, any> {  name: string;
  age?: number;
}let developer: Developer = { name: "semlinker" };
developer.age = 30;
developer.city = "XiaMen";复制代码

三、如何理解函数重载的作用

3.1 可爱又可恨的联合类型

由于 JavaScript 是一个动态语言,我们通常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数而返回不同的类型的调用结果:

function add(x, y) {  return x + y;
}

add(1, 2); // 3add("1", "2"); //"12"复制代码

由于 TypeScript 是 JavaScript 的超集,因此以上的代码可以直接在 TypeScript 中使用,但当 TypeScript 编译器开启 noImplicitAny 的配置项时,以上代码会提示以下错误信息:

Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.复制代码

该信息告诉我们参数 x 和参数 y 隐式具有 any 类型。为了解决这个问题,我们可以为参数设置一个类型。因为我们希望 add 函数同时支持 string 和 number 类型,因此我们可以定义一个 string | number 联合类型,同时我们为该联合类型取个别名:

type Combinable = string | number;复制代码

在定义完 Combinable 联合类型后,我们来更新一下 add 函数:

function add(a: Combinable, b: Combinable) {  if (typeof a === 'string' || typeof b === 'string') {return a.toString() + b.toString();
  }  return a + b;
}复制代码

为 add 函数的参数显式设置类型之后,之前错误的提示消息就消失了。那么此时的 add 函数就完美了么,我们来实际测试一下

const result = add('semlinker', ' kakuqo');
result.split(' ');复制代码

在上面代码中,我们分别使用 'semlinker' 和 ' kakuqo' 这两个字符串作为参数调用 add 函数,并把调用结果保存到一个名为 result 的变量上,这时候我们想当然的认为此时 result 的变量的类型为 string,所以我们就可以正常调用字符串对象上的 split 方法。但这时 TypeScript 编译器又出现以下错误信息了:

Property 'split' does not exist on type 'Combinable'.
Property 'split' does not exist on type 'number'.复制代码

很明显 Combinable 和 number 类型的对象上并不存在 split 属性。问题又来了,那如何解决呢?这时我们就可以利用 TypeScript 提供的函数重载。

3.2 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。

function add(a: number, b: number): number;function add(a: string, b: string): string;function add(a: string, b: number): string;function add(a: number, b: string): string;function add(a: Combinable, b: Combinable) {  // type Combinable = string | number;
  if (typeof a === 'string' || typeof b === 'string') {return a.toString() + b.toString();
  }  return a + b;
}复制代码

在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。

方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:

class Calculator {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: string, b: number): string;
  add(a: number, b: string): string;  add(a: Combinable, b: Combinable) {  if (typeof a === 'string' || typeof b === 'string') {return a.toString() + b.toString();
  }return a + b;
  }
}const calculator = new Calculator();const result = calculator.add('Semlinker', ' Kakuqo');复制代码

这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。

欢迎大家一起交流讨论ts学习过程中遇到的一些问题~~