TypeScript类型之any、unknown

any类型

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

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类型,就相当于是关闭了类型检查,以下的任何一种操作都不会在编译时发生报错:

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类型:

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这两种类型,其实也比较好理解,只有包含所有类型的变量才可能接收一个不确定类型的值。

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,类型的默认操作从都可行变成了几乎都不可行。

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类型的一个例子:

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 的值实际上是某个给定类型,可以使用类型断言,就像这样:

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

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

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

const value: unknown = 42;
const someString: string = value as string;
const otherString = someString.toUpperCase(); // 报错

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

在联合类型中的 unknown 类型

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

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

type UnionType1 = unknown | null; // unknown
type UnionType2 = unknown | undefined; // unknown
type UnionType3 = unknown | string; // unknown
type UnionType4 = unknown | number[]; // unknown

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

type UnionType5 = unknown | any; // any

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

在交叉类型中的 unknown 类型

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

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 作为反序列化后的值的类型。这意味着我们函数的调用者在对返回的值执行操作之前,必须进行某种形式的检查(或者转而使用类型断言)。

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

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 属性:

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…

原文链接:https://juejin.cn/post/7314519123177979916 作者:王渝林

(0)
上一篇 2023年12月21日 上午10:21
下一篇 2023年12月21日 上午10:32

相关推荐

发表回复

登录后才能评论