在编写类型保护时,"任何"和"typeof"的(正确的)惯用替代品是什么


  • 由于TypeScript 3.0在2018年年中引入了unknowntop类型,因此不鼓励使用any类型。
  • TypeScript 也长期支持带有typeof运算符的简洁类型保护,但typeof仅作为标量值(即单个变量)的一流类型保护。
    • 主要警告是,如果不首先使用as anyas 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 );

我的问题是关于我如何习惯地实现isErrorEventTypeScript 语言设计者想要的方式。我无法找到任何关于该主题的权威文档。

具体来说,我不知道我应该如何使用运行时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'?
);

相关内容

  • 没有找到相关文章

最新更新