在编写类型保护时,使用 `typeof` 的 `any` 的(正确)惯用替代方法是啥?

Posted

技术标签:

【中文标题】在编写类型保护时,使用 `typeof` 的 `any` 的(正确)惯用替代方法是啥?【英文标题】:What is the (correct) idiomatic alternative to `any` with `typeof` when writing type-guards?在编写类型保护时,使用 `typeof` 的 `any` 的(正确)惯用替代方法是什么? 【发布时间】:2021-05-19 00:14:31 【问题描述】: 由于 TypeScript 3.0 在 2018 年年中引入了unknown ***类型,因此不鼓励使用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 或目标类型ErrorEvent 的类型断言。据我所知,这些技术中的任何一种都是必需的,因为当 y 不是 x 的静态类型的一部分时,TypeScript 不允许您使用 typeof x.y - 这让我觉得很奇怪,因为 TypeScript 确实 当xany 类型的标量时,您可以使用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 然后允许取消引用e 的成员没有事先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
    );

我想我要问的是如何使这样的事情(见下文)工作,其中 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 还不错的地方之一。类型守卫经常收到 something 并且它必须确定它是否满足成为 something else 的标准。你通常不能静态地做到这一点,否则你并不需要类型保护。在某些情况下,您可以像 A | B 的联合一样执行此操作,并希望为 A 键入保护,但对于任意值,它本质上是不安全的。您可以键入 assert e as Record<string, any> 如果它让您感觉更安全,但我不确定是否有一种方便的方法可以在这里完全安全。 @VLAZ 是的,但我很惊讶 typeof x.yxunknown)是 TypeScript 不允许的——所以我想知道我是否忽略了一些简单的东西。我想我可以检查typeof object 以允许使用任意字符串属性索引器... 可以 更加类型安全并满足编译器的要求,但 IMO 在类型保护中这样做是没有用的。它导致了比较丑陋的代码,这些代码只是为了满足编译器的需要。 See this example。您需要一个 typeguard 来告诉编译器您正在检查的属性可以被检查,然后检查该属性。我觉得它很奇怪而且很迂回。 【参考方案1】:

类型保护是我发现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。如果您想在 一些 类型安全性和编写不那么迟钝的代码之间取得折衷,那么您可以将其键入为 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&lt;PropertyKey, unknown&gt;。没关系,但额外的 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

【讨论】:

Typescript 有 PropertyKey,顺便说一下,它的定义方式与您的 ObjectKey 相同。 @kaya3 非常感谢。尽管我一直在寻找它,但我从未设法找到这种类型。这很有用。

以上是关于在编写类型保护时,使用 `typeof` 的 `any` 的(正确)惯用替代方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript 学习笔记 — 类型推断和类型保护

JS中typeof的用法

typeof关键字

typeof 等价于 PL/SQL 中的对象类型

JavaScript判断变量类型

LayaBox---TypeScript---高级类型