- 原文地址:TypeScript 3.0: The unknown Type
- 原文做者:Marius Schulz
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:shixi-li
- 校对者:Usey95, smilemuffie
TypeScript 3.0 引入了新的unknown
类型,它是 any
类型对应的安全类型。前端
unknown
和 any
的主要区别是 unknown
类型会更加严格:在对 unknown
类型的值执行大多数操做以前,咱们必须进行某种形式的检查。而在对 any
类型的值执行操做以前,咱们没必要进行任何检查。android
这片文章主要关注于 unknown
类型的实际应用,以及包含了与 any
类型的比较。若是须要更全面的代码示例来了解 unknown
类型的语义,能够看看 Anders Hejlsberg 的原始拉取请求。ios
any
类型让咱们首先看看 any
类型,这样咱们就能够更好地理解引入 unknown
类型背后的动机。git
自从 TypeScript 在 2012 年发布第一个版本以来 any
类型就一直存在。它表明全部可能的 JavaScript 值 — 基本类型,对象,数组,函数,Error,Symbol,以及任何你可能定义的值。github
在 TypeScript 中,任何类型均可以被归为 any 类型。这让 any
类型成为了类型系统的 顶级类型 (也被称做 全局超级类型)。typescript
这是一些咱们赋值给 any
类型的代码示例:json
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
类型,能够很容易地编写类型正确可是执行异常的代码。若是咱们使用 any
类型,就没法享受 TypeScript 大量的保护机制。安全
但若是能有顶级类型也能默认保持安全呢?这就是 unknown
到来的缘由。
unknown
类型就像全部类型均可以被归为 any
,全部类型也均可以被归为 unknown
。这使得 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
变量的全部赋值都被认为是类型正确的。
当咱们尝试将类型为 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
复制代码
unknown
类型只能被赋值给 any
类型和 unknown
类型自己。直观的说,这是有道理的:只有可以保存任意类型值的容器才能保存 unknown
类型的值。毕竟咱们不知道变量 value
中存储了什么类型的值。
如今让咱们看看当咱们尝试对类型为 unknown
的值执行操做时会发生什么。如下是咱们以前看过的相同操做:
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
复制代码
将 value
变量类型设置为 unknown
后,这些操做都再也不被认为是类型正确的。经过改变 any
类型到 unknown
类型,咱们的默认设置从容许一切翻转式的改变成了几乎什么都不容许。
这是 unknown
类型的主要价值主张:TypeScript 不容许咱们对类型为 unknown
的值执行任意操做。相反,咱们必须首先执行某种类型检查以缩小咱们正在使用的值的类型范围。
unknown
类型范围咱们能够经过不一样的方式将 unknown
类型缩小为更具体的类型范围,包括 typeof
运算符,instanceof
运算符和自定义类型保护函数。全部这些缩小类型范围的技术都有助于 TypeScript 的基于控制流的类型分析。
如下示例说明了 value
如何在两个 if
语句分支中得到更具体的类型:
function stringifyForLogging(value: unknown): string {
if (typeof value === "function") {
// Within this branch, `value` has type `Function`,
// so we can access the function's `name` property
const functionName = value.name || "(anonymous)";
return `[function ${functionName}]`;
}
if (value instanceof Date) {
// Within this branch, `value` has type `Date`,
// so we can call the `toISOString` method
return value.toISOString();
}
return String(value);
}
复制代码
除了使用 typeof
或 instanceof
运算符以外,咱们还可使用自定义类型保护函数缩小 unknown
类型范围:
/** * A custom type guard function that determines whether * `value` is an array that only contains numbers. */
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)) {
// Within this branch, `unknownValue` has type `number[]`,
// so we can spread the numbers as arguments to `Math.max`
const max = Math.max(...unknownValue);
console.log(max);
}
复制代码
尽管 unknownValue
已经被归为 unknown
类型,请注意它如何依然在 if 分支下获取到 number[]
类型。
unknown
类型使用类型断言在上一节中,咱们已经看到如何使用 typeof
,instanceof
和自定义类型保护函数来讲服 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(); // BOOM
复制代码
这个 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
这个例子。这个类型能够表示任何 unkown 类型或者 string 类型的值。就像咱们以前了解到的,全部类型的值均可以被定义为 unknown
类型,其中也包括了全部的 string
类型,所以,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
复制代码
让咱们回顾一下 IntersectionType3
:unknown & string
类型表示全部能够被同时赋值给 unknown
和 string
类型的值。因为每种类型均可以赋值给 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) {
// 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
是一个被标记的联合类型。在其它语言中,它也能够被称做 Maybe
、Option
或者 Optional
。咱们使用 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
来表示反序列化失败,缘由以下:
null
值是一个有效的 JSON 值。所以,咱们没法区分是对值 null
进行了反序列化,仍是因为缺乏参数或语法错误而致使整个操做失败。null
,咱们没法同时返回错误。所以,咱们函数的调用者不知道操做失败的缘由。为了完整性,这种方法的更成熟的替代方案是使用类型解码器进行安全的 JSON 解析。解码器须要咱们指定要反序列化的值的预期数据结构。若是持久化的JSON结果与该数据结构不匹配,则解码将以明肯定义的方式失败。这样,咱们的函数老是返回有效或失败的解码结果,就再也不须要 unknown
类型了。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。