「译」深入了解TypeScript系列-unknown类型

615 阅读5分钟

TypeScript3.0介绍了新类型unknown这是any类型的类型安全(type-safe)的参照物。 unknow和any的主要区别在与:unknown比any更严格些:在对类型为unknown的值执行大多数操作之前,我们必须做某种形式的检查,而在对类型为any的值执行操作之前,我们不需要做任何检查。 这篇文章着重于unknow类型的实际方面,包括与任何类型的比较。要获得一个显示未知类型语义的全面代码示例,请查看Anders Hejlsberg最初的pull请求。这篇文章着重于未知类型的实际方面,包括与任何类型的比较。要获得一个显示未知类型语义的全面代码示例,请查看Anders Hejlsberg最初的pull请求。

any类型

让我们先看看any类型,以便更好地理解引入unknown类型背后的动机。 any类型自2012年第一次发布以来就一直在TypeScript中。它代表了所有可能的JavaScript值原语,对象,数组,函数,错误,符号,等等。 在TypeScript中,每种类型都可以赋值给any。这使得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

any类型本质上是类型系统的一个逃生舱口。作为开发人员,这给了我们很大的自由:TypeScript允许我们对any类型的值执行任何想要的操作,而不需要事先执行任何类型的检查。 在上面的例子中,value变量的类型为any。因此,TypeScript认为以下所有操作都是正确的类型

let value: any;

value.foo.bar;  // OK
value.trim();   // OK
value();        // OK
new value();    // OK
value[0][1];    // OK

在很多情况下,这是太宽容了。使用any类型,很容易编写类型正确的代码,但在运行时存在问题。如果我们选择使用TypeScript,我们不会得到很多保护。如果有一个顶部类型默认是安全的呢?这就是unknown发挥作用的地方。

unknown类型

就像所有类型都可以赋值给any一样,所有类型都可以赋值给unknown。这使得TypeScript类型系统的另一个顶级类型(另一个是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

对value变量的所有赋值都被认为是类型正确的。 但是,当我们试图将unkonwn类型的值赋给其他类型的变量时,会发生什么呢

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

unknown类型只能赋值给any类型和未知类型本身。直观地说,这是有意义的:只有能够保存任意类型值的容器才能保存未知类型的值;毕竟,我们不知道什么类型的价值存储在价值中。 现在让我们看看当我们尝试对unknown类型的值执行操作时会发生什么。这是我们之前看过的相同的操作。

let value: unknown;
value.foo.bar;  // Error
value.trim();   // Error
value();        // Error
new value();    // Error
value[0][1];    // Error

由于value变量类型为unknow,这些操作都不再被认为是类型正确的。通过从“any”变为“unknown”,我们将默认设置从允许所有内容转变为(几乎)不允许任何内容。这是unknown类型的主要值命题:TypeScript不会让我们对未知类型的值执行任意操作。相反,我们必须首先执行某种类型检查,以缩小正在处理的值的类型。

缩小unknown类型

我们可以用不同的方法将unknown类型缩小为更特定的类型,包括typeof操作符、instanceof操作符和自定义类型保护函数。所有这些窄化技术都有助于TypeScript基于控制流的类型分析。 下面的例子说明了value如何在两个if语句分支中具有更特定的类型

function stringifyForLogging(value: unknown): string {
  if (typeof value === "function") {
    // 在此分支中,“值”的类型为“function”,
    // 这样我们就可以访问函数的`name`属性
    const functionName = value.name || "(anonymous)";
    return `[function ${functionName}]`;
  }
  if (value instanceof Date) {
    // 在此分支中,“值”的类型为“Date”,
    // 所以我们可以调用`toISOString`方法
    return value.toISOString();
  }
  return String(value);
}

除了使用typeof或instanceof操作符之外,我们还可以使用自定义类型保护函数来缩小unknown类型

/**
 * 一个自定义类型保护函数,用于确定是否
 * value 是否为只包含数字的数组。
 */
function isNumberArray(value: unknown): value is number[] {
  return (
    Array.isArray(value) &&
    value.every(element => typeof element === "number")
  );
}
const unknownValue: unknown = [15, 23, 8, 4, 42, 16];
if (isNumberArray(unknownValue)) {
  // 在这个分支中,' unknownValue '的类型是' number '
  // 因此,我们可以将这些数字作为参数分散到' Math.max '
  const max = Math.max(...unknownValue);
  console.log(max);
}

请注意,尽管unknownValue被声明为unknown类型,但它在if语句分支中是如何具有number[]类型的。请注意,尽管unknownValue被声明为未知类型,但它在if语句分支中是如何具有number[]类型的。

使用unknown类型断言

在上一节中,我们已经看到了如何使用typeof、instanceof和自定义类型保护函数来让TypeScript编译器相信一个值具有特定的类型。这是将未知类型的值缩小到更特定类型的安全且推荐的方法。 如果希望强制编译器相信类型未知的值是给定类型,可以使用类似这样的类型断言 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();  // BOOM

value变量保存一个数字,但是我们使用类型断言值作为字符串来假装它是一个字符串。小心使用类型断言!

联合类型中的unknown类型

现在让我们看看如何在联合类型中处理未知类型。在下一节中,我们还将研究交集类型。 在联合类型中,未知会吸收所有类型。这意味着,如果任何组成类型是unknown,则联合类型的计算结果为unknown。

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

这条规则有一个例外。如果至少有一个组成类型为any,则union类型的计算结果为any

type UnionType5 = unknown | any;  // any

那么,为什么unknown吸收了所有类型(除了any)呢?让我们考虑未知|字符串的例子。该类型表示所有可分配给未知类型的值加上可分配给string类型的值。正如我们之前所了解的,所有类型都可以赋值给unknown。这包括所有的字符串,因此,unknown | string表示与未知本身相同的值集。因此,编译器可以将union类型简化为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

让我们来看看IntersectionType3: unknown & string类型代表了所有可分配给unknown和string的值。由于每种类型都可赋值给unknown,因此在交集类型中包含unknown不会改变结果。只剩下string类型。

使用带有unknown类型值的操作符

类型unknown的值不能用作大多数操作符的操作数。这是因为,如果不知道所处理的值的类型,大多数操作符都不太可能产生有意义的结果。 对于未知类型的值,唯一可以使用的操作符是四个相等和不相等操作符 • === • == • !== • != 如果希望对类型为未知的值使用任何其他操作符,则必须首先缩小类型范围(或使用类型断言强制编译器信任您)。

示例:从本地存储中读取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) {
    // The item does not exist, thus return an error result
    return {
      success: false,
      error: new Error(`Item with key "${key}" does not exist`)
    };
  }
  let value: unknown;
  try {
    value = JSON.parse(item);
  } catch (error) {
    // The item is not valid JSON, thus return an error result
    return {
      success: false,
      error
    };
  }
  // Everything's fine, thus return a success result
  return {
    success: true,
    value
  };
}

返回的类型结果是带标记的联合类型(也称为区别的联合类型)。在其他语言中,它也被称为“可能”、“选项”或“可选”。我们使用Result来清晰地模拟成功和不成功的操作结果。 tryDeserializeLocalStorageItem函数的调用者必须在尝试使用value或error属性之前检查success属性

const result = tryDeserializeLocalStorageItem("dark_mode");
if (result.success) {
  // We've narrowed the `success` property to `true`,
  // so we can access the `value` property
  const darkModeEnabled: unknown = result.value;
  if (typeof darkModeEnabled === "boolean") {
    // We've narrowed the `unknown` type to `boolean`,
    // so we can safely use `darkModeEnabled` as a boolean
    console.log("Dark mode enabled: " + darkModeEnabled);
  }
} else {
  // We've narrowed the `success` property to `false`,
  // so we can access the `error` property
  console.error(result.error);
}

请注意,由于以下两个原因,tryDeserializeLocalStorageItem函数不能简单地返回null来表示反序列化失败

  1. null是一个有效的JSON值。因此,我们将无法区分反序列化的值是null,还是由于缺少项或语法错误而导致整个操作失败。
  2. 如果我们要从函数返回null,我们不能同时返回错误。因此,函数的调用者将不知道操作失败的原因。 为了完整起见,这种方法的一种更复杂的替代方法是使用类型化解码器进行安全的JSON解析。解码器允许我们指定要反序列化的值的预期模式。如果持久化的JSON结果与该模式不匹配,则解码将以良好定义的方式失败。这样,我们的函数总是返回有效或失败的解码结果,我们可以完全消除unknown类型。

原文 mariusschulz.com/blog/the-un…