TypeScript类型之any、unknown

any类型

先来说说any类型,在类型系统中存在top type的概念,就是最顶层的类型,其他类型是这个顶层类型的字类型。any就是一种顶层类型,可以将其他任何类型的值赋给any类型的变量:

typescript 复制代码
let value: any; 
value = true; // OK
value = 42; // OK 
value = "Hello World"; // OK 
value = []; // OK 
value = {}; // OK 
value = Math.random; // OK 
value = null; // OK 
value = undefined; // OK 
value = new TypeError(); // OK 
value = Symbol("type"); // OK

在typescript中使用any类型,就相当于是关闭了类型检查,以下的任何一种操作都不会在编译时发生报错:

typescript 复制代码
let value: any; 
value.foo.bar; // OK 
value.trim(); // OK 
value(); // OK 
new value(); // OK 
value[0][1]; // OK

开发中使用any确实很容易写出符合类型规范的代码,但是大量使用就失去了类型检查的好处,导致运行时出现大量bug。因此也引发了我的好奇,既然使用any会让类型检查失效,干嘛还要引入这个类型呢?

为什么需要any类型

后面查了下资料才发现,引入 any 类型的主要原因有一些历史和实用性的考虑:

  1. 平滑迁移: TypeScript 最初是为 JavaScript 代码添加类型检查而设计的,而 JavaScript 是一门非常灵活的动态语言。为了让开发者能够逐步引入类型检查而不破坏现有代码,引入了 any 类型。这使得开发者可以在 TypeScript 项目中逐步迁移 JavaScript 代码而不会立即遇到大量的类型错误。
  2. 与动态语言的互操作性: 有些库或框架可能并没有提供完整的 TypeScript 类型定义,或者是通过动态类型来实现某些功能。在这种情况下,使用 any 类型可以更容易地与这些库进行集成。
  3. 不明确类型: 有时候,某个值的类型可能非常复杂或难以明确表示,或者我们可能并不关心其具体类型。使用 any 类型可以简化代码,让 TypeScript 编译器对该值进行较少的类型检查。

正如我们上面看到的那样,使用any会让我们失去类型检查的优势,也破坏了类型推断,导致编译器无法推断变量的类型,使得开发者失去了一些代码提示和自动完成的功能。所以开发中应该尽量避免使用any类型。

unknown类型

那如果确实有些类型在最开始使用的时候无法确认,我们该怎么办呢,TypeScript 3.0针对这种场景引入了一个新类型unknown,它像是any的兄弟类型,也可以将其他任何类型的值赋给unknown类型:

typescript 复制代码
let value: unknown; 
value = true; // OK 
value = 42; // OK 
value = "Hello World"; // OK 
value = []; // OK 
value = {}; // OK 
value = Math.random; // OK 
value = null; // OK 
value = undefined; // OK 
value = new TypeError(); // OK 
value = Symbol("type"); // OK

但是当我们把其他类型的值赋给unknown类型的时候,发现只允许any以及unknown这两种类型,其实也比较好理解,只有包含所有类型的变量才可能接收一个不确定类型的值。

typescript 复制代码
let value: unknown; 
let value1: unknown = value; // OK 
let value2: any = value; // OK 
let value3: boolean = value; // Error 
let value4: number = value; // Error 
let value5: string = value; // Error 
let value6: object = value; // Error 
let value7: any[] = value; // Error 
let value8: Function = value; // Error

再来对比之前使用any时做的一些操作,发现此时的类型检查都不通过,从anyunknown,类型的默认操作从都可行变成了几乎都不可行。

typescript 复制代码
let value: unknown; 
value.foo.bar; // Error 
value.trim(); // Error 
value(); // Error 
new value(); // Error 
value[0][1]; // Error

这是 unknown 类型的主要优势:TypeScript 不允许我们对 unknown 类型的值执行任意操作。相反,我们必须首先执行某种类型检查,以缩小我们正在处理的值的类型。

收窄 unknown 类型

以下是使用typeof收窄unknown类型的一个例子:

typescript 复制代码
function processValue(value: unknown): string {
    // 使用 typeof 进行类型检查
    if (typeof value === 'string') {
        return value.toUpperCase(); // 在这里,TypeScript 知道 value 的类型是 string
    } else {
        return 'Unknown value'; // 在这里,TypeScript 知道 value 的类型不是 string
    }
}

// 调用函数
console.log(processValue("Hello")); // 输出: HELLO
console.log(processValue(42));      // 输出: Unknown value

使用 unknown 类型的类型断言

在前面的部分中,我们已经了解了如何使用 typeof来让 TypeScript 编译器相信一个值具有特定的类型。这是缩小 unknown 类型到更具体类型的安全且推荐的方式。

如果想要强制编译器相信某个类型为 unknown 的值实际上是某个给定类型,可以使用类型断言,就像这样:

typescript 复制代码
const value: unknown = "Hello World";
const someString: string = value as string;
const otherString = someString.toUpperCase(); // "HELLO WORLD"

请注意,TypeScript 不会执行任何特殊检查以确保类型断言实际上是有效的。类型检查器假设你知道得更好,并信任你在类型断言中使用的任何类型都是正确的。

这可能会导致运行时错误,如果你犯了错误并指定了不正确的类型:

typescript 复制代码
const value: unknown = 42;
const someString: string = value as string;
const otherString = someString.toUpperCase(); // 报错

value 变量保存了一个数字,但我们正在假装它是一个字符串,使用类型断言 value as string。在使用类型断言时,请小心!

在联合类型中的 unknown 类型

现在让我们看看 unknown 类型在联合类型中的处理方式。在下一部分,我们还将看到交叉类型。

在联合类型中,unknown 吸收每个类型。这意味着如果任何一个组成类型是 unknown,那么联合类型的结果就是 unknown

typescript 复制代码
type UnionType1 = unknown | null; // unknown
type UnionType2 = unknown | undefined; // unknown
type UnionType3 = unknown | string; // unknown
type UnionType4 = unknown | number[]; // unknown

这个规则的唯一例外是 any。如果至少有一个组成类型是 any,那么联合类型就是 any

typescript 复制代码
type UnionType5 = unknown | any; // any

那么为什么 unknown 吸收每个类型(除了 any)呢?让我们思考一下 unknown | string 的例子。这个类型表示所有可分配给 unknown 类型的值,再加上那些可分配给 string 类型的值。正如我们之前学过的,所有类型都可以分配给 unknown。这包括所有字符串,因此 unknown | string 表示的是与 unknown 本身相同的值集合。因此,编译器可以将联合类型简化为 unknown

在交叉类型中的 unknown 类型

在交叉类型中,每个类型都吸收 unknown。这意味着将任何类型与 unknown 进行交叉操作并不会改变结果类型:

typescript 复制代码
type IntersectionType1 = unknown & null; // null
type IntersectionType2 = unknown & undefined; // undefined
type IntersectionType3 = unknown & string; // string
type IntersectionType4 = unknown & number[]; // number[]
type IntersectionType5 = unknown & any; // any

让我们看看 IntersectionType3unknown & string 类型表示所有可分配给 unknownstring 的值。由于每种类型都可以分配给 unknown,包括 unknown 不会改变结果。我们最终得到的仍然是 string

在值为 unknown 类型的情况下使用运算符

unknown 类型的值不能用作大多数运算符的操作数。这是因为如果我们不知道我们要处理的值的类型,那么大多数运算符可能无法产生有意义的结果。

在类型为 unknown 的值上,唯一可以使用的运算符是四个相等和不等运算符:

  • ===
  • ==
  • !==
  • !=

如果想在类型为 unknown 的值上使用任何其他运算符,必须首先缩小类型(或使用类型断言,强制编译器相信你)。

例子:从 localStorage 读取 JSON

这里有一个使用 unknown 类型的真实示例。

假设我们想编写一个函数,它从 localStorage 中读取一个值并将其反序列化为 JSON。如果该项不存在或不是有效的 JSON,函数应返回一个错误结果;否则,它应将值反序列化并返回。

由于我们不知道在反序列化持久化的 JSON 字符串之后会得到什么类型的值,我们将使用 unknown 作为反序列化后的值的类型。这意味着我们函数的调用者在对返回的值执行操作之前,必须进行某种形式的检查(或者转而使用类型断言)。

下面是我们如何实现该函数:

typescript 复制代码
type Result =
  | { success: true; value: unknown }
  | { success: false; error: Error };

function tryDeserializeLocalStorageItem(key: string): Result {
  const item = localStorage.getItem(key);

  if (item === null) {
    // 该项不存在,因此返回一个错误结果
    return {
      success: false,
      error: new Error(`Item with key "${key}" does not exist`),
    };
  }

  let value: unknown;

  try {
    value = JSON.parse(item);
  } catch (error) {
    // 该项不是有效的 JSON,因此返回一个错误结果
    return {
      success: false,
      error,
    };
  }

  // 一切正常,因此返回一个成功的结果
  return {
    success: true,
    value,
  };
}

返回类型 Result 是一个带标签的联合类型。在其他语言中,它也被称为 Maybe、Option 或 Optional。我们使用 Result 来清晰地模型化操作的成功和不成功的结果。

调用 tryDeserializeLocalStorageItem 函数的调用者在尝试使用 valueerror 属性之前必须检查 success 属性:

typescript 复制代码
const result = tryDeserializeLocalStorageItem("dark_mode");

if (result.success) {
  // 我们已经将 `success` 属性缩小到 `true`,
  // 因此我们可以访问 `value` 属性
  const darkModeEnabled: unknown = result.value;

  if (typeof darkModeEnabled === "boolean") {
    // 我们已经将 `unknown` 类型缩小到 `boolean`,
    // 因此我们可以安全地将 `darkModeEnabled` 用作布尔值
    console.log("Dark mode enabled: " + darkModeEnabled);
  }
} else {
  // 我们已经将 `success` 属性缩小到 `false`,
  // 因此我们可以访问 `error` 属性
  console.error(result.error);
}

请注意,tryDeserializeLocalStorageItem 函数不能简单地返回 null 来表示反序列化失败,原因有两点:

  1. null 是一个有效的 JSON 值。因此,我们无法区分是反序列化了 null 还是整个操作由于缺少项或语法错误而失败。
  2. 如果我们从函数中返回 null,我们无法同时返回错误。因此,我们函数的调用者将不知道操作失败的原因。

总结

anyunknown是一对兄弟类型,any放弃类型检查,默认所有行为都是合法的,而unknown默认所有行为都是不合法的。 使用unknown类型时,需要搭配其他方式收窄类型,确保后续使用是合法的。 另外还介绍了交叉以及联合类型中unknown的表现形式,最后以一个例子说明了实际情况中应该如何使用unknown

参考: dozie.dev/difference-...

mariusschulz.com/blog/the-un...

相关推荐
MiyueFE20 小时前
🚀🚀五个前端开发者都应该了解的TS技巧
前端·typescript
ttod_qzstudio1 天前
基于typescript严格模式以实现undo和redo功能为目标的命令模式代码参考
typescript·命令模式
张志鹏PHP全栈1 天前
TypeScript 第十天,TypeScript面向对象之Class(二)
前端·typescript
慧一居士1 天前
ESLint 完整功能介绍和完整使用示例演示
前端·javascript·typescript
enzeberg2 天前
TypeScript 工具类型(Utility Types)
typescript
難釋懷2 天前
TypeScript类
前端·typescript
杰哥焯逊2 天前
基于TS封装的高德地图JS APi2.0实用工具(包含插件类型,基础类型)...持续更新
前端·javascript·typescript
工业甲酰苯胺3 天前
TypeScript枚举类型应用:前后端状态码映射的最简方案
javascript·typescript·状态模式
土豆骑士4 天前
简单理解Typescript 装饰器
前端·typescript
ttod_qzstudio4 天前
彻底移除 HTML 元素:element.remove() 的本质与最佳实践
前端·javascript·typescript·html