问题描述
- 由于 TypeScript 3.0 在 2018 年年中引入了
unkNown
top type,因此不鼓励使用any
类型。 - TypeScript 长期以来一直支持使用
typeof
运算符的简洁类型保护,但typeof
仅用作标量值(即单变量)的一流类型保护。
我目前正在用 TypeScript 编写客户端错误处理代码,特别是,我正在为 window.error
编写一个事件侦听器,它接收一个 ErrorEvent
对象,而该对象又具有一个名为 error
的成员属性,实际上可以根据不同的情况使用任何东西。
在 TypeScript 中,我们需要编写用作运行时和编译时类型保护的顶级函数。例如,要检查 window.error
事件侦听器是否真的收到 ErrorEvent
而不是 Event
,我会这样写:
function isErrorEvent( e: unkNown ): e is ErrorEvent {
// Todo
}
function onWindowError( e: unkNown ): void {
if( isErrorEvent( e ) ) {
// do stuff with `e.error`,etc.
}
}
window.addEventListener( 'error',onWindowError );
我的问题是关于我打算如何惯用实现 isErrorEvent
TypeScript 语言设计者想要的方式。我还没有找到有关该主题的任何权威文档。
具体来说,我不知道我应该如何使用运行时 typeof
检查来实现 isErrorEvent
,而不使用对 any
或目标类型 { 的类型断言{1}}。据我所知,这两种技术中的任何一种都是必需的,因为当 ErrorEvent
不是 typeof x.y
的静态类型的一部分时,TypeScript 不会让您使用 y
- 这让我感到奇怪,因为TypeScript 确实允许您在 x
是 任何 类型的标量时使用 typeof x
,而不仅仅是它的静态类型。
下面,使用 x
有效,但我不喜欢 as any
属性取消引用缺乏安全性:
asAny.colno
另一种方法是使用 function isErrorEvent( e: unkNown ): e is ErrorEvent {
if( !e ) return;
const asAny = e as any;
return (
typeof asAny.colno === 'number' &&
typeof asAny.error === 'object' &&
typeof asAny.lineno === 'number'
);
}
,但我觉得这同样不安全,因为 TypeScript 然后允许取消引用 as ErrorEvent
的成员,而无需事先{{1 }}检查!
e
我想我要问的是如何使这样的事情(见下文)工作,其中 TypeScript 要求在允许任何取消引用之前使用 typeof
检查每个成员,并允许 function isErrorEvent( e: unkNown ): e is ErrorEvent {
if( !e ) return;
const assumed = e as ErrorEvent;
return (
typeof assumed.colno === 'number' &&
typeof assumed.error === 'object' &&
typeof assumed.lineno === 'number' &&
// For example,TypeScript will not complain about the line below,even though I haven't proved that `e.message` actually exists,just because `ErrorEvent.message` is defined in `lib.dom.d.ts`:
assumed.message.length > 0
);
}
将其静态类型保留为 typeof
:
e
...但是 TypeScript 确实让我们这样做(见下文),这可以说是同一件事,只是在语法上更加冗长:
unkNown
解决方法
类型保护是我发现 any
完全可以接受的少数几个地方之一。根据它们的参数,您基本上有两种类型的类型保护
- 他们采用许多东西,通常是联合(例如,
A | B | C
)并缩小联合范围(例如,到B
)。 - 他们采用一个不为人所知的东西它是什么并赋予它形状。
在前一种情况下,您可以轻松地在类型系统的范围内工作以缩小范围。
在后一种情况下,您可以使用不同数量的“无形”,但在极端情况下(例如您的 unknown
),您没有类型支持,这会导致看起来有点难看。看这里:
type HasProp<T extends object,K extends string> = T & {[P in K]: unknown};
/*
* type guard to ensure that an arbitrary object has a given property
*/
function hasProp<T extends object,K extends string>(obj: T,prop: K): obj is HasProp<T,K> {
return prop in obj;
}
function isErrorEvent( e: unknown ): e is ErrorEvent {
if( !e ) return false;
if (
typeof e === "object" && //type guard determines `e` is `object | null`
e !== null //type guard to narrow down `e` to only `object`
) {
if (
hasProp(e,"colno") && //type guard to add `colno` to the properties known to be in `e`
hasProp(e,"error") && //type guard to add `error` to the properties known to be in `e`
hasProp(e,"lineno") && //type guard to add `lineno` to the properties known to be in `e`
hasProp(e,"message") //type guard to add `message` to the properties known to be in `e`
){
return (
typeof e.colno === 'number' &&
typeof e.error === 'object' &&
typeof e.lineno === 'number' &&
typeof e.message === 'string' &&
e.message.length > 0
);
}
}
return false;
}
我想说清楚——这段代码所做的所有操作都是正确的。如果 e
不是对象,则无法检查它是否具有某些任意属性。如果不检查属性是否存在,检查任意属性值是否为给定类型有点无用。
话虽如此,但它过于冗长,也有点迟钝。
-
e !== null
没有用,因为它在开始时已经由!e
处理了。 - 检查一个属性是否存在以检查其值是否为数字直接等同于检查该值是否为数字。通常没有区别 - 如果属性不存在,其值是不同的类型,那么它最终都是一样的。
因此,我个人很乐意将 e
输入为 any
。如果您想在 some 类型安全性和编写不那么笨拙的代码之间取得折衷,那么您可以将其用作 Record
function isObj(obj: unknown): obj is Record<PropertyKey,unknown> {
return typeof obj === "object" && obj !== null;
}
function isErrorEvent( e: unknown ): e is ErrorEvent {
if ( isObj(e) ) {
return (
typeof e.colno === 'number' &&
typeof e.error === 'object' &&
typeof e.lineno === 'number' &&
typeof e.message === 'string' &&
e.message.length > 0
);
}
return false;
}
对我来说,上面的代码更容易阅读和理解。它没有被编译器严格检查,但它也是完全正确的。这也是 acts exactly the same when using any
因此我不反对它。只要您对对象进行了适当的检查,它是 Record
还是 any
都无关紧要。无论哪种方式,您都不会从编译器获得任何类型支持。后者在类型方面稍微更正确,但是否有所不同取决于您。
注 1:您还可以使用类型断言 e as Record<PropertyKey,unknown>
。无关紧要,但额外的 isObj
类型保护似乎更有可能被重用。
注意 2:只是为了记录,可以更改 hasProp
以应用于多个属性。它没有解决我在类型保护中使用它的核心问题,但它可能在其他地方仍然有用:
/*
* type guard to ensure that an arbitrary object has a given properties
*/
function hasProps<T extends object,K extends PropertyKey>(obj: T,...props: K[]): obj is HasProp<T,K> {
return props.every(prop => prop in obj);
}
/* ... */
if (hasProps(e,"colno","error","lineno","message")) { //type guard to add `colno`,`error`,`lineno`,`message` to the properties known to be in `e`
/* ... */