微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在编写类型保护时,带有 `typeof` 的 `any` 的正确惯用替代方法是什么?

如何解决在编写类型保护时,带有 `typeof` 的 `any` 的正确惯用替代方法是什么?

  • 由于 TypeScript 3.0 在 2018 年年中引入了 unkNown top type,因此不鼓励使用 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 );

我的问题是关于我打算如何惯用实现 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;
}

Playground Link

我想说清楚——这段代码所做的所有操作都是正确的。如果 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;
}

Playground Link

对我来说,上面的代码更容易阅读和理解。它没有被编译器严格检查,但它也是完全正确的。这也是 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`
/* ... */

Playground Link

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。