- 由于TypeScript 3.0在2018年年中引入了
unknown
top类型,因此不鼓励使用any
类型。 - TypeScript 也长期支持带有
typeof
运算符的简洁类型保护,但typeof
仅作为标量值(即单个变量)的一流类型保护。- 主要警告是,如果不首先使用
as any
或as T
,它不能与对象属性或数组元素一起使用。- 使用
as any
立即会出现明显的问题。 - 但是使用
as T
也会引入自己的问题。这在类型保护函数中并不是一个大问题,因为假设类型的变量的范围仅限于类型保护,但如果在普通函数中使用,它可能会引入错误。
- 使用
- 主要警告是,如果不首先使用
我目前正在 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
或目标类型ErrorEvent
。据我所知,这两种技术中的任何一种都是必需的,因为当 TypeScript 不属于x
静态类型时y
不会让你使用typeof x.y
- 这让我感到奇怪,因为 TypeScript确实允许你在x
是任何类型的标量时使用typeof 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'
);
}
另一种方法是使用as ErrorEvent
,但我觉得这同样不安全,因为 TypeScript允许在没有事先typeof
检查的情况下取消引用e
的成员!
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
);
}
我想我要问的是我怎样才能使这样的东西(见下文)工作,其中 TypeScript 要求在允许任何取消引用之前用typeof
检查每个成员,并允许e
保留其静态类型unknown
:
function isErrorEvent( e: unknown ): e is ErrorEvent {
if( !e ) return;
return (
typeof e.colno === 'number' &&
typeof e.error === 'object' &&
typeof e.lineno === 'number' &&
typeof e.message === 'string' &&
e.message.length > 0
);
}
。但是 TypeScript确实让我们这样做(见下文),这可以说是同一件事,只是语法上要详细得多:
function isErrorEvent( e: unknown ): e is ErrorEvent {
if( !e ) return;
const assume = e as ErrorEvent;
if(
typeof e.colno === 'number' &&
typeof e.error === 'object' &&
typeof e.lineno === 'number' &&
)
{
const message = assume.message as any;
return typeof message === 'string' && message.length > 0;
}
}
类型防护是我发现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
。如果你想在某些类型安全和不太迟钝的代码之间做出折衷,那么你可以使用类型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;
}
游乐场链接
对我来说,上面的代码更容易阅读和理解。编译器没有严格检查它,但它也是完全正确的。使用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`
/* ... */
游乐场链接
//例如,TypeScript 不会抱怨下面的行,即使我没有证明
e.message
确实存在,只是因为它在lib.dom.d.ts
中定义了ErrorEvent.message
在这种情况下,您不应该试图证明它正是某个具体类的实例,而应该只定义它的功能的一些特定的、狭窄的子集,您实际上有兴趣使用它们,然后对该特定形状进行类型检查。
例如,你并不真正感兴趣,如果e
是一个实际的ErrorEvent
实例,你只关心它是否符合一些狭隘的契约:
interface IErrorEvent {
message: string;
lineno: number;
colno: number;
}
现在,您只需要对该确切的合同进行类型保护。 我想出的最佳解决方案是使用一种类似于@VLAZ的方法,只需向isObj
助手添加一个用于推理的泛型,以便在编写缩小类型检查时获得一些拼写错误保护:
const isLike = <T extends object>(o: unknown): o is Record<keyof T, unknown> => (
(o ?? false) && typeof o === 'object'
);
const isErrorEvent = (e: unknown): e is IErrorEvent => (
isLike<IErrorEvent>(e)
// e at this point is a { message: unknown; lineno: unknown; colno: unknown }, not IErrorEvent or ErrorEvent
&& (typeof e.message === 'string') // ok
&& (typeof e.lineno === 'number') // ok
&& (typeof e.collno === 'number') // TS2551: Property 'collno' does not exist on type 'Record '. Did you mean 'colno'?
);