typescript 在编写类型保护时,用“typeof”替换“any”的(正确的)惯用方法是什么?

uubf1zoe  于 2023-04-22  发布在  TypeScript
关注(0)|答案(2)|浏览(147)
  • 由于TypeScript 3.0在2018年年中引入了unknowntop type,因此不鼓励使用any类型。
  • TypeScript也长期支持使用typeof运算符的succint类型保护,但typeof仅用作标量值(即单个变量)的第一类类型保护。
  • 主要的警告是如果不先使用as anyas T,它不能与对象属性或数组元素一起使用
  • 使用as any有明显的问题。
  • 但是使用as T也会带来它自己的问题。在类型保护函数中这不是一个大问题,因为变量的作用域仅限于类型保护,但是如果在普通函数中使用,它会引入bug。

我目前正在用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 );

我的问题是关于我如何按照TypeScript语言设计者的意图实现isErrorEvent。我还没有找到任何关于这个主题的权威文档。
具体来说,我不知道如何使用运行时typeof检查来实现isErrorEvent,而不使用any或目标类型ErrorEvent的类型Assert。这两种技术都是必需的,因为当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;
    }
}
efzxgjgh

efzxgjgh1#

类型保护是我发现any可以接受的少数地方之一。

  • 它们采用许多东西,通常是一个并集(例如,A | B | C),并缩小并集(例如,B)。
  • 他们把一个不为人所知的东西 * 它是什么 * 并给予它形状。

在前一种情况下,您可以轻松地在类型系统的范围内工作以缩小范围。
在后一种情况下,你有不同数量的“shapeless”来处理,但在极端情况下(比如你的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链接
我想明确一点-这段代码所做的所有操作都是 * 正确的 *。如果e不是一个对象,你就不能检查它是否有一些任意的属性。如果不检查属性是否存在,检查一个任意的属性值是否是一个给定的类型是有点没用的。
话虽如此,它是过于冗长,也有点迟钝。

  • e !== null是无用的,因为它在一开始就已经被!e处理了。
  • 检查属性是否存在以检查其值是否为数字直接等同于检查值是否为数字。通常没有区别-如果属性不存在或其值是不同的类型,则最终都是相同的。

因此,我个人很乐意将e键入为any

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链接
对我来说,上面的代码更容易阅读和理解。它不像编译器那样严格检查,但它也是完全正确的。当使用any时,它的行为也完全相同,因此我不反对它。只要你做适当的检查,你有一个对象,不管是Record还是any都没有什么关系。无论哪种方式,你都不会从编译器获得任何类型支持。后者在类型方面稍微正确一些,但是否有区别取决于你。
注1:你也可以使用类型Asserte 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链接

bwntbbo3

bwntbbo32#

//例如,TypeScript不会抱怨下面这行,即使我没有证明e.message实际存在,仅仅因为ErrorEvent.message是在lib.dom.d.ts中定义的
在这种情况下,你不应该试图证明它是某个具体类的示例,而只需要定义一些特定的,狭窄的特性子集,你实际上有兴趣使用,然后对特定的形状进行类型检查。
例如,你并不真正感兴趣,如果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'?
);

相关问题