【问题标题】:Check types of an array in typescript在打字稿中检查数组的类型
【发布时间】:2019-12-27 11:31:37
【问题描述】:

显然,Typescript 似乎与 AST 配合得很好。如果我检查x.type == "Abc",那么下一行,打字稿就知道x 的类型是Abc。请注意,我使用它来检查带有 JSDOC 格式类型注释的 JS 文件。但我想纯打字稿文件也是如此

但是,我在测试一组元素时遇到了问题。

第一个示例有效,因为我循环遍历每个元素,并且仅在检查类型时推送它。所以 typescript 正确推断出类型 Property[] 作为函数的返回类型

/**
 * @param {ObjectExpression} objectAst
 */
function getPropertiesList(objectAst) {
    let propertiesList = []
    for (let p of objectAst.value.properties) {
        if (p.type == "Property")
            propertiesList.push(p)
        else
            throw new Error("Properties field has elements that aren't of type `Property`")
    }
    return propertiesList
}

但是,这个示例在功能上是相同的(但在我看来更干净并且不会创建新数组)不起作用。推断类型为(SpreadElement|Property|ObjectMethod|ObjectProperty|SpreadProperty)[]。所以它不考虑支票。

/**
 * @param {ObjectExpression} objectAst
 */
function getPropertiesList(objectAst) {
    let propertiesList = objectAst.value.properties
    if (!propertiesList.every(p => p.type == "Property"))
        throw new Error("Properties field has elements that aren't of type `Property`")
    return propertiesList
}

谁能提供一些关于打字稿如何以不同方式处理一个案例的见解?

Typescript 可以使用检查来使特定类型更加具体(如第一个示例所示),但显然它无法对数组执行这些检查。

这可以被认为是打字稿编译器中的一个错误(因为两段代码显然应该返回相同的类型)?

编辑:为了提供一些上下文和可测试性,我从recast 导入了如下类型:

/**
 * @typedef { import('recast').types.namedTypes.ObjectExpression} ObjectExpression 
 * @typedef { import('recast').types.namedTypes.Property} Property 
*/

【问题讨论】:

    标签: typescript abstract-syntax-tree


    【解决方案1】:

    问题是编译器不理解array.every() 可以用作type guard 类型的array。此外,回调函数p => p.type == "Property" 也不会被推断为p 类型的类型保护。编译器非常擅长分析潜在类型缩小的内联代码,但当控制流传递到函数时,它几乎是gives up (see microsoft/TypeScript#9998)

    如果您希望 TypeScript 了解调用 boolean-returning 函数充当类型保护,则需要手动将此类函数注释为 user-defined type guard。像foo(x: T): boolean 这样的函数可以更改为foo(x: T): x is U,其中“x is U" 是一个类型谓词。如果foo(val) 返回true,那么编译器会将val 缩小为@987654340 @. 否则不会。

    对于回调,这需要将p => p.type == "Property" 更改为(p): p is Property => type == "Property"。对于array.every(),那个方法就是Array<T> 接口内的declared in the standard library。幸运的是,您可以使用merge in extra method overloads to interfaces(请注意,如果您的代码在模块中,您可能必须专门使用global augmentation 来添加到像Array<T> 这样的全局接口)。它看起来像这样:

    interface Array<T> {
        every<U extends T>(cb: (x: T) => x is U): this is Array<U>;
    }
    

    现在编译器将看到如果回调是一个类型保护函数,那么every() 本身就充当了一个类型保护。您的代码将按需要运行:

    function getPropertiesList(objectAst: ObjectAST): Property[] {
        let propertiesList = objectAst.value.properties
        if (!propertiesList.every((p): p is Property => p.type == "Property"))
            throw new Error("Properties field has elements that aren't of type `Property`")
        return propertiesList
    }
    

    不过,对于单次使用 every() 而言,这可能工作量太大。在实践中,您可能应该只使用type assertion 并继续前进。类型断言适用于您比编译器更了解类型的情况;这是一个合理的使用时间:

    function getPropertiesListAssert(objectAst: ObjectAST): Property[] {
        let propertiesList = objectAst.value.properties
        if (!propertiesList.every(p => p.type == "Property"))
            throw new Error("Properties field has elements that aren't of type `Property`")
        return propertiesList as Property[]; // assert
    }
    

    好的,希望对您有所帮助;祝你好运!

    Playground Link to code

    【讨论】:

    • 非常感谢您的回答。它给了我更多的洞察力和更多的阅读内容。
    猜你喜欢
    • 2021-04-03
    • 2018-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-29
    • 2018-12-27
    • 2021-01-09
    • 1970-01-01
    相关资源
    最近更新 更多