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来表示反序列化失败
- null是一个有效的JSON值。因此,我们将无法区分反序列化的值是null,还是由于缺少项或语法错误而导致整个操作失败。
- 如果我们要从函数返回null,我们不能同时返回错误。因此,函数的调用者将不知道操作失败的原因。
为了完整起见,这种方法的一种更复杂的替代方法是使用类型化解码器进行安全的JSON解析。解码器允许我们指定要反序列化的值的预期模式。如果持久化的JSON结果与该模式不匹配,则解码将以良好定义的方式失败。这样,我们的函数总是返回有效或失败的解码结果,我们可以完全消除unknown类型。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!