如何在 TypeScript 中使用类型守卫实现任何功能
本文最初发表于我的博客:camchenry.com。如果您喜欢这篇文章,请考虑加入我的邮件列表,以便获取更多类似内容。
TypeScript 的价值在于它使我们能够编写安全的代码。因为当代码中的每个类型在编译时都已知时,我们可以使用 TypeScript 编译代码并执行类型检查,从而确保代码不会崩溃或导致错误。
然而,并非总能在编译时知道所有类型,例如从外部 API 接收任意数据时。为了在运行时检查类型或区分不同类型的数据,我们需要使用类型守卫来缩小类型范围。
什么是收窄?
在 TypeScript 中,类型缩减是指将宽泛的类型细化为更窄的类型。类型缩减非常有用,因为它允许代码接受多种类型。然后,我们可以使用类型守卫将类型缩减为更有用的类型。
以下是一些常见的缩小范围的例子:
unknown或anystringstring | object | number到stringnumber | null | undefined到numberstring自定义类型NonEmptyString
什么是类型保护符?
类型守卫是一种条件检查,用于缩小类型范围。类型守卫允许使用表达式进行运行时类型检查,以确定值是否属于特定类型。
那么,字形保护符长什么样呢?以下都是字形保护符的示例:
typeof value === 'string''name' in datavalue instanceof MouseEvent!value
类型守卫是一种特殊的表达式,它可以改变变量的类型。我们稍后会看更多类型守卫的实际应用示例。
字形保护器的种类(如何检查字形)
大多数类型守卫都围绕着常规的 JavaScript 运算符展开,TypeScript 为这些运算符赋予了额外的功能,使得编写典型的 JavaScript 代码即可实现类型缩减。因此,你很可能在不知不觉中就已经使用过类型守卫了!
从根本上讲,每个类型守卫都依赖于检查某个表达式的计算结果是否为真或假。
因此,我们要介绍的第一种类型守卫是简单的真值检查。但我们可以使用更复杂的类型守卫,例如 `is_true` in、typeof`is_false` 和instanceof`is_true`,它们可以提供更多信息。
除了所有这些内置的类型保护之外,我们还可以更进一步,创建我们自己的自定义类型保护,以检查任何类型。
布尔类型守卫(真值)
如前所述,检查值的真假是所有类型守卫的本质。
然而,布尔类型守卫只检查值的真假,除此之外不提供任何其他信息。其他更复杂的类型守卫可以检查更复杂的类型或验证更多属性,但布尔类型守卫是最基本的类型守卫。
function getAvailableRooms(rooms: number | undefined) {
if (rooms) {
return `There are ${rooms} hotel rooms available to book.`;
}
return "Sorry, all rooms are currently booked.";
}
getAvailableRooms(undefined); // "Sorry, all rooms are currently booked."
getAvailableRooms(5); // "There are 5 hotel rooms available to book."
使用布尔类型保护时,值会被隐式转换为布尔值。这在大多数情况下都有合理的解释,但并非总是如此。
例如,如果使用布尔类型守卫来检查类型number | undefined,我们可能预期它只会排除值为 ` undefinednull` 的情况。然而,它也会排除值为 0 的情况,这在某些情况下可能并非我们所期望的。有关此常见错误的更多信息,请参阅 Kent C. Dodd 的文章“在 JSX 中使用三元运算符而不是 `&& `”。
等式守卫
在布尔类型守卫中,我们检查表达式的真值。在相等类型守卫中,我们检查表达式的值。
当我们知道某个类型的所有可能值时,这种类型守卫就很有用。例如,如果我们有一个字符串或数值枚举,或者如果我们想知道某个值不是空字符串或null数字undefined。
下面举个例子,我们使用相等类型守卫来从变量类型中移除特定值: undefined
function getGreeting(timeOfDay?: "morning" | "afternoon") {
if (timeOfDay === undefined) {
return `Hello!`;
}
// Now the type of `timeOfDay` is narrowed to `morning` | `afternoon`,
// so we can use string methods on it safely.
return `Good ${timeOfDay[0].toUpperCase()}${timeOfDay.slice(1)}!`;
}
getGreeting(); // "Hello!"
getGreeting("afternoon"); // "Good Afternoon!"
getGreeting("morning"); // "Good Morning!"
我们也可以使用switch代码块来实现同样的功能:
function getGreeting(timeOfDay?: "morning" | "afternoon") {
switch (timeOfDay) {
case "afternoon":
case "morning":
return `Good ${timeOfDay[0].toUpperCase()}${timeOfDay.slice(1)}!`;
default:
return `Hello!`;
}
}
switch如果您有很多可能的值需要检查,并且这些值可能共享相同的代码,那么使用这样的代码块可能更合适。
受歧视的工会值得单独撰文讨论,但它们本质上是平等型卫士的更强大版本。
可区分联合类型是一种包含多种可能类型的类型,它通过一个字段来区分这些类型。换句话说,当我们检查某个字段的值(例如 `type` type)时,它会自动包含其他一些属性。
typeof类型保护
与前面的例子(我们检查变量(或表达式)的值typeof)不同,使用类型守卫检查变量的类型。
当有一个值有几种可能的类型时,例如string | number,我们可以使用typeof来确定它是哪种类型。
例如,我们可以使用 ` typeoftf.js` 编写一个比较函数,该函数比较两个值并返回它们的差值:
function compare(a: number | string, b: number | string): number {
if (typeof a === "number" && typeof b === "number") {
// Both a and b are numbers, so we can compare them directly.
return a - b;
}
if (typeof a === "string" && typeof b === "string") {
// We can use string methods on `a` and `b` safely.
return a.localeCompare(b);
}
throw new Error(
`Cannot compare unrelated types '${typeof a}' and '${typeof b}'`
);
}
compare("a", "b"); // => -1
compare("b", "a"); // => 1
compare(123, 321); // => -198
该守卫的最大局限性typeof在于它只能区分JavaScript 可识别的类型。可检查的类型typeof包括:
booleanstringnumberbigintobjectsymbolfunctionundefined
instanceof类型保护
当有一个变量是某个类的实例时,我们可以使用 `typeof`instanceof来检查该变量是否具有该类型。
例如,DOM API 定义了许多类和子类,可以使用以下方法快速检查instanceof:
function handleEvent(event: Event) {
if (event instanceof MouseEvent) {
// `event` now has type `MouseEvent`, so we can access mouse-specific properties
console.log(`A mouse event occurred at (${event.x}, ${event.y}`);
}
if (event instanceof KeyboardEvent) {
// `event` now has type `KeyboardEvent`, so we can access key-specific properties
console.log(`A keyboard event occurred: ${event.key} ${event.}`);
}
console.log("An event occurred: ", event.type);
}
这在处理可能通用的 DOM 对象时非常有用,因为只需一次instanceof检查即可访问该类的所有属性和方法。
这也可以用于区分 JavaScript 中的常见对象,例如 `int` Map、Date`int`、Array`int` 或 `int` Set。例如,我们可以创建一个函数来创建一个查找表,该查找表可以接受多种可能的输入:
// Creates a Map which returns some value given a string key
// (ignoring the fact that the Map constructor already accepts some of these)
function createLookupTable<Value>(
db: [string, Value][] | Map<string, Value> | Record<string, Value>
): Map<string, Value> {
// `db` has type `[string, Value][] | Map<string, Value> | Record<string, Value>`
if (db instanceof Array) {
// `db` now has type `[string, Value][]`
return new Map(db);
}
// `db` has type `Map<string, Value> | Record<string, Value>`
if (db instanceof Map) {
// `db` now has type `Map<string, Value>`
return db;
}
// `db` has type `Record<string, Value>`
return new Map(Object.entries(db));
}
createLookupTable([
["hat", 14.99],
["shirt", 24.95],
]);
// => Map (2) {"hat" => 14.99, "shirt" => 24.95}
createLookupTable(
new Map([
["hat", 14.99],
["shirt", 24.95],
])
);
// => Map (2) {"hat" => 14.99, "shirt" => 24.95}
createLookupTable({ hat: 14.99, shirt: 24.95 });
// => Map (2) {"hat" => 14.99, "shirt" => 24.95}
以下是另一个使用 ` instanceofT` 来检查类型是 `a`Date还是 `b`string并决定是否构造新Date对象的示例:
function getDate(value: string | Date): Date {
if (value instanceof Date) {
return value;
}
return new Date(value);
}
getDate("2021-05-06 03:25:00");
// => Date: "2021-05-06T07:25:00.000Z"
getDate(new Date("2021-05-06 03:25:00"));
// => Date: "2021-05-06T07:25:00.000Z"
in类型保护
类型in守卫允许我们通过检查对象是否具有特定属性来区分多种类型。在 JavaScript 中,in与所有类型守卫一样,`if` 运算符返回一个布尔值,指示对象是否具有该属性。例如:
"data" in { name: "test", data: { color: "blue" } }; // => true
"data" in { name: "test", data: undefined }; // => true
"data" in { name: "test" }; // => false
这样,我们就可以利用这种方法in来区分具有不同属性集的对象。例如,我们可以用它来区分不同类型的类(在本例中为事件):
function handleEvent(event: MouseEvent | KeyboardEvent) {
if ("key" in event) {
// event now has type `KeyboardEvent`
console.log(`A keyboard event occurred: ${event.key}`);
} else {
// event now has type `MouseEvent`
console.log(`A mouse event occurred: ${event.button}`);
}
}
这里需要注意的是,`is`key仅针对 `a` 定义KeyboardEvent,而未针对 `b` 定义MouseEvent。如果我们要检查的属性在多个情况下都存在,则范围缩小将不起作用。例如,以下代码将无法正常工作:
type EventInput =
| { type: "mouse"; button: string }
| { type: "key"; key: string };
function handleEventInput(event: EventInput) {
// This type guard will NOT work:
if ("type" in event) {
// event still has type `EventInput`, so the type guard does not
// do any narrowing in this case
}
}
虽然并非总是与缩小类型有关,但该in运算符也经常用于检查浏览器对某些功能的支持情况。
例如,该守卫会'serviceWorker' in navigator检查浏览器是否支持 Service Worker。
断言类型守卫(或断言函数)
TypeScript 3.7 版本新增了对断言函数的支持。断言函数是一种假定某个条件始终为真的函数,当条件不成立时,它会抛出一个错误。
要创建断言函数,我们需要添加一个叫做“断言签名”的东西,它是对函数要断言的内容的正式声明。断言签名是关于函数的附加信息(类似于返回类型),它允许 TypeScript 编译器缩小类型范围。
我们来看一个例子:
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new TypeError(`Expected 'string', got: '${typeof value}'`);
}
}
const x = "123";
assertString(x);
// x now has type 'string', so it is safe to use string methods
x.toLowerCase();
之前我们讨论过,所有类型守卫都基于布尔值检查。这一点在这个例子中仍然成立,但实际用法与其他类型守卫略有不同。
对于其他类型的守卫,我们通常使用类似 ` ifor` 这样的语句switch来创建不同的执行分支。而对于断言函数,只有两个分支:正常继续执行,或者停止脚本(抛出错误)。
除了断言类型守卫抛出异常的方式不同之外,断言类型守卫与其他类型守卫类似。但是,我们必须注意避免意外创建断言错误条件的类型守卫。
这就是导致我们产生虚假安全感的其中一种方法。例如,函数会断言某些内容,但实际代码却什么也没断言。
function assertString(value: unknown): asserts value is string {
// This check does not match the assertion signature
if (typeof value === "boolean") {
throw new TypeError();
}
}
const x: unknown = 123;
assertString(x);
// We get a run-time exception here (!!!), which TypeScript should
// be able to prevent under normal circumstances:
x.toLowerCase();
// "TypeError: x.toLowerCase is not a function"
用户自定义类型守卫
大多数类型守卫都有其检查范围的限制,例如只能检查基本类型typeof,或者只能检查类instanceof。但使用用户自定义类型守卫,则没有任何检查范围的限制。
自定义类型守卫是最强大的类型守卫,因为我们可以验证任何类型,包括我们自己定义的类型,以及 JavaScript 或 DOM 内置的类型。自定义类型守卫的主要缺点是它们不是预定义的,所以我们必须自己编写。
不过,也有一些内置的自定义类型保护程序,例如Array.isArray:
const data: unknown = ["a", "b", 123, false];
if (Array.isArray(data)) {
// data now has type "array", so it is safe to use array methods
data.sort();
}
下一节,我们将探讨定义我们自己的类型保护函数的所有不同方法。
类型保护函数
类型守卫函数是一个返回值并具有类型谓词的函数。
类型谓词是添加到函数中的额外声明(类似于返回类型),它为 TypeScript 提供额外信息,并允许 TypeScript 缩小变量的类型范围。例如,在 `function` 的定义中Array.isArray,
function isArray(arg: any): arg is any[];
类型谓词是arg is any[]。用口头语言来说,这个函数的签名可能是:“isArray接受一个类型为的参数any,并检查它是否是数组。”一般来说,类型谓词的形式为:variable is type。
一个函数要符合类型保护函数的条件,必须满足以下条件:
- 返回布尔值
- 具有类型谓词
类型谓词取代了返回类型,因为带有类型谓词的函数必须始终返回布尔值。
类型保护函数示例
检查值是否为字符串
这个例子本质上是内置typeof类型保护的可重用形式。
function isString(value: unknown): value is string {
return typeof value === "string";
}
检查值是否已定义(非空或未定义)
Type | null类型守卫的一个常见用例是将类似 `null`或 ` Type | undefinedundefined`的类型细化为Type`null` 或 `undefined`,从而有效地消除 `null` 或 `undefined` 的情况。我们可以通过接受一个可以为 `null` 或 `undefined` 的泛型类型,并添加一个类型谓词来null | undefined从类型中移除 `null` 或 `undefined` 来实现这一点。
function isDefined<Value>(value: Value | undefined | null): value is Value {
return value !== null && value !== undefined;
}
然后,就可以这样使用:
const x: string | undefined = 123;
if (isDefined(x)) {
// x is defined, so it is safe to use methods on x
x.toLowerCase();
}
从数组中移除所有值null或值undefined
利用isDefined我们刚刚定义的类型守卫,我们可以将其与内置Array.filter函数一起使用,该内置函数对类型谓词有特殊的支持。该Array.filter函数定义如下:
function filter<Filtered extends Item>(
predicate: (value: Item, index: number, array: Item[]) => value is Filtered
): Filtered[];
(此处的定义略有修改,以提高理解性和可读性。)本质上,Array.filter`is type guard` 的每次使用都是一种类型保护,但大多数情况下,调用前后的类型Array.filter必须相同。
但是,如果传递给函数的参数Array.filter 缩小了类型范围(例如使用类型守卫),那么返回类型就会Array.filter改变。因此,我们可以使用isDefined类型守卫从数组中移除所有 `int`null和 ` undefinedint` 值,以及从数组元素中移除 `int`null和 ` int` 类型。undefined
// 'values' is an array of strings, but can have null or undefined values
const values: (string | null | undefined)[] = [null, "a", "b", undefined];
// We can safely assign 'filtered' to an array of strings (string[])
// because `isDefined` changes the type of the variable 'values'
const filtered: string[] = values.filter(isDefined);
检查一个数是否为正数
创建自定义类型的一个常见用途是确保满足某些条件。例如,我们可能希望确保对象具有某些属性、字符串不为空或数字为正数。
首先,我们需要创建一个自定义PositiveNumber类型,以及一个类型守卫来检查它。
type PositiveNumber = number & { __type: "PositiveNumber" };
function isPositive(n: number): n is PositiveNumber {
return n >= 0;
}
为了创建一种新的数字类型,我们使用一种称为“类型品牌化”的技术。本质上,我们为数字类型添加一个虚拟属性,以将其与其他所有数字类型区分开来。在本例中,我选择使用 `[]` { __type: 'PositiveNumber' },但我们可以选择任何任意键值对,只要它是唯一的且尚未被定义即可。
重要的是,我们不能通过PositiveNumber声明变量来创建对象:
const x: PositiveNumber = 49;
// ERROR: Type 'number' is not assignable to type 'PositiveNumber
这或许看起来不太方便,但这正是它能够让我们编写安全代码的原因,因为我们必须始终使用类型守卫来检查条件,从而防止我们编写出如下代码:
const x: PositiveNumber = -100;
举例来说,我们可以编写一个只接受正数的平方根函数,来说明如何使用这种类型保护:
function squareRoot(n: PositiveNumber): PositiveNumber {
return Math.sqrt(n) as PositiveNumber;
}
然后,我们可以使用类型守卫来计算平方根:
const x = 49;
squareRoot(x);
// ERROR: ^^^ 'number' is not assignable to parameter of type 'PositiveNumber'
if (isPositive(x)) {
// OK: Now x has type 'PositiveNumber', so we can take the square root
squareRoot(x);
}
检查字符串是否为 GUID
与前面的例子类似,我们可以创建一个Guid基于该string类型的自定义类型,并编写一个类型守卫来检查它。
type Guid = string & { __type: "Guid" };
const guidPattern =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function isGuid(value: string): value is Guid {
return guidPattern.test(value);
}
为了举例说明如何在实践中使用此类型和类型守卫,我们将创建一个可以通过 GUID 搜索的用户列表。
type User = {
id: Guid;
name: string;
};
const users: User[] = [
/* ... */
];
function getUserById(id: Guid) {
return users.find((user) => user.id === id);
}
const id = "abc123";
getUserById(id);
// ERROR: ^^ Argument of type 'string' is not assignable to parameter of type 'Guid'
if (isGuid(id)) {
// OK: id now has type `Guid`:
getUserById(id);
}
检查值是否为有效的 React 元素(React.isValidElement)
React 自带的函数isValidElement会检查一个值是否是有效的 React 元素,即 React 可以渲染的元素。
function isValidElement<P>(
object: {} | null | undefined
): object is ReactElement<P>;
该函数的具体实现与此处无关,但它是一个典型的类型保护函数示例,该函数可以验证其他类型保护无法验证的自定义类型。
自定义类型防护功能的优缺点
自定义类型保护函数功能强大,有时甚至是编写类型安全代码的唯一选择。然而,它们编写起来可能比较棘手,而且容易出错。
自定义类型保护函数的优点是:
- 灵活性:可以检查任何类型,包括我们定义的自定义类型。
- 运行时类型检查:允许在运行时进行类型检查,确保代码在编译时和运行时都安全可靠。
- 可重用:类型保护函数允许我们将多个类型保护合并为一个,并轻松地在多个地方使用它们。
自定义类型保护函数的缺点是:
- 手动:类型保护函数必须手动编写(目前没有自动生成类型保护的方法)
- 性能:使用类型保护函数会略微增加调用函数和运行检查的开销(实际上可以忽略不计)。
- 脆弱性:自定义类型的保护措施可能因意外错误而实施,从而造成虚假的安全感。
字形保护罩可以在哪些地方使用?
现在我们已经了解了所有可用的类型保护符,接下来我们将简要介绍一下类型保护符的使用场景。类型保护符的使用场景有限。最常见的用法是在代码if/else块中,例如:
if (typeof value === "string") {
// value has type 'string' in this block
} else {
// value does NOT have type 'string' in this block
}
既然我们可以在代码块中使用类型守卫if/else,那么你可能会认为我们也可以在三元运算符中使用类型守卫,因为它是代码if/else块的简写形式。你的想法是对的!
typeof value === 'string'
? /* value has type 'string' in this block */
: /* value does NOT have type 'string' in this block */
此外,自TypeScript 4.4起,我们可以使用别名条件的类型守卫。
const isString = typeof value === "string";
if (isString) {
// value has type 'string' in this block
} else {
// value does NOT have type 'string' in this block
}
除了类型保护之外if/else,类型保护也可以在代码块中使用while:
while (typeof value === "string") {
// value has type 'string' in this block
}
最后,类型保护符也与代码块兼容switch/case:
switch (typeof value) {
case "string":
// value has type 'string' in this block
break;
}
结论
类型守卫是一种条件检查,它允许类型从一种类型细化到另一种类型,从而使我们能够编写既类型安全又易于编写的代码。由于 TypeScript 是 JavaScript 的超集,许多常用运算符(例如 `&`typeof或 `& instanceof`)都充当类型守卫。但是,只要付出足够的努力,我们也可以使用自定义类型守卫来验证任何条件和任何类型。
概括
一般来说,我建议使用感觉最自然的类型保护方式,这需要经验积累。如果简单的typeof检查就能满足需求,就不要编写自定义类型保护函数。但是,有时编写自定义类型保护函数也是必要的。
为了总结每种类型守卫的优势,以下是一个汇总表。
| 类型保护 | 用法 |
|---|---|
| 布尔值/真值 | 排除诸如null、undefined、''、0等错误值。 |
| 平等 | 将多种可能的类型缩小到单一类型。 |
typeof |
将类型缩小到基本类型(例如string或number) |
instanceof |
检查某个值是否是特定类的实例 |
in |
检查该属性是否可以访问 |
| 断言函数 | 断言那些始终成立的不变式。 |
| 自定义类型的保护功能 | 检查类型是否满足某些任意条件 |
如果这篇文章对您有帮助,请在推特上@cammchenry告诉我!如果您喜欢这类指南,请考虑订阅我的邮件列表,以便在新文章发布时收到通知。
祝你好运,编程愉快!
文章来源:https://dev.to/camchenry/how-to-do-anything-in-typescript-with-type-guards-24h7